package fs

import (
	"errors"
	"io/fs"
	"math"
	"os"
	"syscall"

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

// IsSpecialFSFromFileInfo determines if a file resides on a special file
// system (e.g. /proc, /dev/, /sys) based on its fs.FileInfo.
// The bool return value should be ignored if err is not nil.
func IsSpecialFSFromFileInfo(st fs.FileInfo) (bool, error) {
	// On Linux, special file systems like /proc, /dev/, and /sys are
	// considered unnamed devices (non-device mounts). These devices
	// will always have a major device number of 0 per the kernels
	// Documentation/admin-guide/devices.txt file.

	getDevId := func(st fs.FileInfo) (uint64, error) {
		st_sys_any := st.Sys()
		if st_sys_any == nil {
			return 0, errors.New("nil returned by fs.FileInfo.Sys")
		}

		st_sys, ok := st_sys_any.(*syscall.Stat_t)
		if !ok {
			return 0, errors.New("could not convert st.sys() to a *syscall.Stat_t")
		}
		return st_sys.Dev, nil
	}

	devId, err := getDevId(st)
	if err != nil {
		return false, err
	}
	if unix.Major(devId) != 0 {
		// This file is definitely not on a special file system.
		return false, nil
	}

	// We know the file is in a special file system, but we'll make an
	// exception for tmpfs, which might be used at a variety of mount points.
	// Since the minor IDs are assigned dynamically, we'll find the device ID
	// for each common tmpfs mount point. If the mount point's device ID matches this st's,
	// then it is reasonable to assume the file is in tmpfs. If the device ID
	// does not match, then st is not located in that special file system so we
	// can't give an exception based on that file system root. This check is still
	// valid even if the directory we check against isn't mounted as tmpfs, because
	// the device ID won't match so we won't grant a tmpfs exception based on it.
	// On Linux, every tmpfs mount has a different device ID, so we need to check
	// against all common ones that might be in use.
	tmpfsMounts := []string{"/tmp", "/run", "/dev/shm"}
	if tmpdir := os.TempDir(); tmpdir != "/tmp" {
		tmpfsMounts = append(tmpfsMounts, tmpdir)
	}
	if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
		tmpfsMounts = append(tmpfsMounts, xdgRuntimeDir)
	}
	getFileDevId := func(n string) (uint64, error) {
		fSt, err := os.Stat(n)
		if err != nil {
			return math.MaxUint64, err
		}
		fDevId, err := getDevId(fSt)
		if err != nil {
			return math.MaxUint64, err
		}
		return fDevId, nil
	}
	var errs []error
	for _, fn := range tmpfsMounts {
		// Don't stop if getFileDevId returns an error. It could
		// be because the tmpfsMount we're checking doesn't exist,
		// which shouldn't prevent us from checking the other
		// potential mount points.
		if fnDevId, err := getFileDevId(fn); err == nil {
			if fnDevId == devId {
				return false, nil
			}
		} else if !errors.Is(err, os.ErrNotExist) {
			// Ignore errors for missing mount points.
			errs = append(errs, err)
		}
	}

	// We didn't find any a reason to give st a special file system exception.
	return true, errors.Join(errs...)
}
