//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris

package exec

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/sys/unix"
)

// secureOpenRegularFile anchors traversal at a trusted base directory fd,
// opens every intermediate component with openat(O_DIRECTORY|O_NOFOLLOW), then
// opens and verifies the final regular file through the same descriptor.
func secureOpenRegularFile(base, path string, maxBytes int64) (*os.File, error) {
	rel, err := filepath.Rel(base, path)
	if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
		return nil, errSecureFileRejected
	}
	components := strings.Split(filepath.Clean(rel), string(filepath.Separator))
	if len(components) == 0 {
		return nil, errSecureFileRejected
	}

	dirfd, err := unix.Open(base, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
	if err != nil {
		if errors.Is(err, unix.ENOENT) {
			return nil, errSecureFileNotFound
		}
		return nil, fmt.Errorf("%w: open base: %v", errSecureFileRejected, err)
	}
	defer func() { _ = unix.Close(dirfd) }()

	for _, component := range components[:len(components)-1] {
		if component == "" || component == "." || component == ".." {
			return nil, errSecureFileRejected
		}
		nextfd, openErr := unix.Openat(dirfd, component, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
		if openErr != nil {
			if errors.Is(openErr, unix.ENOENT) {
				return nil, errSecureFileNotFound
			}
			return nil, fmt.Errorf("%w: open directory component: %v", errSecureFileRejected, openErr)
		}
		if closeErr := unix.Close(dirfd); closeErr != nil {
			_ = unix.Close(nextfd)
			return nil, fmt.Errorf("close directory component: %w", closeErr)
		}
		dirfd = nextfd
	}

	final := components[len(components)-1]
	if final == "" || final == "." || final == ".." {
		return nil, errSecureFileRejected
	}
	fd, err := unix.Openat(dirfd, final, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
	if err != nil {
		if errors.Is(err, unix.ENOENT) {
			return nil, errSecureFileNotFound
		}
		return nil, fmt.Errorf("%w: open file component: %v", errSecureFileRejected, err)
	}
	file := os.NewFile(uintptr(fd), path)
	if file == nil {
		_ = unix.Close(fd)
		return nil, fmt.Errorf("wrap secure file descriptor")
	}

	info, err := file.Stat()
	if err != nil {
		_ = file.Close()
		return nil, fmt.Errorf("fstat secure file: %w", err)
	}
	if !info.Mode().IsRegular() {
		_ = file.Close()
		return nil, errSecureFileRejected
	}
	if info.Size() > maxBytes {
		_ = file.Close()
		return nil, errSecureFileTooLarge
	}
	return file, nil
}

func secureReadRegularFile(base, path string, maxBytes int64) ([]byte, error) {
	file, err := secureOpenRegularFile(base, path, maxBytes)
	if err != nil {
		return nil, err
	}
	defer file.Close()
	blob, err := io.ReadAll(io.LimitReader(file, maxBytes+1))
	if err != nil {
		return nil, fmt.Errorf("read secure file: %w", err)
	}
	if int64(len(blob)) > maxBytes {
		return nil, errSecureFileTooLarge
	}
	return blob, nil
}
