//go:build solaris
// +build solaris

package xattr

import (
	"errors"
	"os"

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

const (
	// XATTR_SUPPORTED will be true if the current platform is supported
	XATTR_SUPPORTED = true

	XATTR_CREATE  = 0x1
	XATTR_REPLACE = 0x2

	// ENOATTR is not defined on Solaris. When attempting to open an
	// extended attribute that doesn't exist, we'll get ENOENT. For
	// compatibility with other platforms, we make ENOATTR available as
	// an alias of unix.ENOENT.
	ENOATTR = unix.ENOENT
)

func getxattr(path string, name string, data []byte) (int, error) {
	f, err := openNonblock(path)
	if err != nil {
		return 0, err
	}
	defer func() {
		_ = f.Close()
	}()
	return fgetxattr(f, name, data)
}

func lgetxattr(path string, name string, data []byte) (int, error) {
	return 0, unix.ENOTSUP
}

func fgetxattr(f *os.File, name string, data []byte) (int, error) {
	fd, err := unix.Openat(int(f.Fd()), name, unix.O_RDONLY|unix.O_XATTR, 0)
	if err != nil {
		return 0, err
	}
	defer func() {
		_ = unix.Close(fd)
	}()
	return unix.Read(fd, data)
}

func setxattr(path string, name string, data []byte, flags int) error {
	f, err := openNonblock(path)
	if err != nil {
		return err
	}
	err = fsetxattr(f, name, data, flags)
	if err != nil {
		_ = f.Close()
		return err
	}
	return f.Close()
}

func lsetxattr(path string, name string, data []byte, flags int) error {
	return unix.ENOTSUP
}

func fsetxattr(f *os.File, name string, data []byte, flags int) error {
	mode := unix.O_WRONLY | unix.O_XATTR
	if flags&XATTR_REPLACE != 0 {
		mode |= unix.O_TRUNC
	} else if flags&XATTR_CREATE != 0 {
		mode |= unix.O_CREAT | unix.O_EXCL
	} else {
		mode |= unix.O_CREAT | unix.O_TRUNC
	}
	fd, err := unix.Openat(int(f.Fd()), name, mode, 0666)
	if err != nil {
		return err
	}
	if _, err = unix.Write(fd, data); err != nil {
		_ = unix.Close(fd)
		return err
	}
	return unix.Close(fd)
}

func removexattr(path string, name string) error {
	mode := unix.O_RDONLY | unix.O_XATTR | unix.O_NONBLOCK | unix.O_CLOEXEC
	fd, err := unix.Open(path, mode, 0)
	if err != nil {
		return err
	}
	f := os.NewFile(uintptr(fd), path)
	defer func() {
		_ = f.Close()
	}()
	return fremovexattr(f, name)
}

func lremovexattr(path string, name string) error {
	return unix.ENOTSUP
}

func fremovexattr(f *os.File, name string) error {
	fd, err := unix.Openat(int(f.Fd()), ".", unix.O_XATTR, 0)
	if err != nil {
		return err
	}
	defer func() {
		_ = unix.Close(fd)
	}()
	return unix.Unlinkat(fd, name, 0)
}

func listxattr(path string, data []byte) (int, error) {
	f, err := openNonblock(path)
	if err != nil {
		return 0, err
	}
	defer func() {
		_ = f.Close()
	}()
	return flistxattr(f, data)
}

func llistxattr(path string, data []byte) (int, error) {
	return 0, unix.ENOTSUP
}

func flistxattr(f *os.File, data []byte) (int, error) {
	fd, err := unix.Openat(int(f.Fd()), ".", unix.O_RDONLY|unix.O_XATTR, 0)
	if err != nil {
		// When attempting to list extended attributes on a filesystem
		// that doesn't support them (like as UFS and tmpfs), we'll get
		// EINVAL. Translate this error to the more conventional ENOTSUP.
		if errors.Is(err, unix.EINVAL) {
			return 0, unix.ENOTSUP
		}
		return 0, err
	}
	xf := os.NewFile(uintptr(fd), f.Name())
	defer func() {
		_ = xf.Close()
	}()
	names, err := xf.Readdirnames(-1)
	if err != nil {
		return 0, err
	}
	var buf []byte
	for _, name := range names {
		buf = append(buf, append([]byte(name), '\000')...)
	}
	if data == nil {
		return len(buf), nil
	}
	return copy(data, buf), nil
}

// Like os.Open, but passes O_NONBLOCK to the open(2) syscall.
func openNonblock(path string) (*os.File, error) {
	fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
	if err != nil {
		return nil, err
	}
	return os.NewFile(uintptr(fd), path), err
}

// stringsFromByteSlice converts a sequence of attributes to a []string.
// We simulate Linux/Darwin, where each entry is a NULL-terminated string.
func stringsFromByteSlice(buf []byte) (result []string) {
	offset := 0
	for index, b := range buf {
		if b == 0 {
			result = append(result, string(buf[offset:index]))
			offset = index + 1
		}
	}
	return
}
