/*
Package xattr provides support for extended attributes on linux, darwin and freebsd.
Extended attributes are name:value pairs associated permanently with files and directories,
similar to the environment strings associated with a process.
An attribute may be defined or undefined. If it is defined, its value may be empty or non-empty.
More details you can find here: https://en.wikipedia.org/wiki/Extended_file_attributes .

All functions are provided in triples: Get/LGet/FGet, Set/LSet/FSet etc. The "L"
variant will not follow a symlink at the end of the path, and "F" variant accepts
a file descriptor instead of a path.

Example for "L" variant, assuming path is "/symlink1/symlink2", where both components are
symlinks:
Get will follow "symlink1" and "symlink2" and operate on the target of
"symlink2". LGet will follow "symlink1" but operate directly on "symlink2".
*/
package xattr

import (
	"os"
	"syscall"
)

// Error records an error and the operation, file path and attribute that caused it.
type Error struct {
	Op   string
	Path string
	Name string
	Err  error
}

func (e *Error) Unwrap() error { return e.Err }

func (e *Error) Error() (errstr string) {
	if e.Op != "" {
		errstr += e.Op
	}
	if e.Path != "" {
		if errstr != "" {
			errstr += " "
		}
		errstr += e.Path
	}
	if e.Name != "" {
		if errstr != "" {
			errstr += " "
		}
		errstr += e.Name
	}
	if e.Err != nil {
		if errstr != "" {
			errstr += ": "
		}
		errstr += e.Err.Error()
	}
	return
}

// Get retrieves extended attribute data associated with path. It will follow
// all symlinks along the path.
func Get(path, name string) ([]byte, error) {
	return get(path, name, func(name string, data []byte) (int, error) {
		return getxattr(path, name, data)
	})
}

// LGet is like Get but does not follow a symlink at the end of the path.
func LGet(path, name string) ([]byte, error) {
	return get(path, name, func(name string, data []byte) (int, error) {
		return lgetxattr(path, name, data)
	})
}

// FGet is like Get but accepts a os.File instead of a file path.
func FGet(f *os.File, name string) ([]byte, error) {
	return get(f.Name(), name, func(name string, data []byte) (int, error) {
		return fgetxattr(f, name, data)
	})
}

type getxattrFunc func(name string, data []byte) (int, error)

// get contains the buffer allocation logic used by both Get and LGet.
func get(path string, name string, getxattrFunc getxattrFunc) ([]byte, error) {
	const (
		// Start with a 1 KB buffer for the xattr value
		initialBufSize = 1024

		// The theoretical maximum xattr value size on MacOS is 64 MB. On Linux it's
		// much smaller: documented at 64 KB. However, at least on TrueNAS SCALE, a
		// Debian-based Linux distro, it can be larger.
		maxBufSize = 64 * 1024 * 1024

		// Function name as reported in error messages
		myname = "xattr.get"
	)

	size := initialBufSize
	for {
		data := make([]byte, size)
		read, err := getxattrFunc(name, data)

		// If the buffer was too small to fit the value, Linux and MacOS react
		// differently:
		// Linux: returns an ERANGE error and "-1" bytes. However, the TrueNAS
		//   SCALE distro sometimes returns E2BIG.
		// MacOS: truncates the value and returns "size" bytes. If the value
		//   happens to be exactly as big as the buffer, we cannot know if it was
		//   truncated, and we retry with a bigger buffer. Contrary to documentation,
		//   MacOS never seems to return ERANGE!
		// To keep the code simple, we always check both conditions, and sometimes
		// double the buffer size without it being strictly necessary.
		if err == syscall.ERANGE || err == syscall.E2BIG || read == size {
			// The buffer was too small. Try again.
			size <<= 1
			if size >= maxBufSize {
				return nil, &Error{myname, path, name, syscall.EOVERFLOW}
			}
			continue
		}
		if err != nil {
			return nil, &Error{myname, path, name, err}
		}
		return data[:read], nil
	}
}

// Set associates name and data together as an attribute of path.
func Set(path, name string, data []byte) error {
	if err := setxattr(path, name, data, 0); err != nil {
		return &Error{"xattr.Set", path, name, err}
	}
	return nil
}

// LSet is like Set but does not follow a symlink at
// the end of the path.
func LSet(path, name string, data []byte) error {
	if err := lsetxattr(path, name, data, 0); err != nil {
		return &Error{"xattr.LSet", path, name, err}
	}
	return nil
}

// FSet is like Set but accepts a os.File instead of a file path.
func FSet(f *os.File, name string, data []byte) error {
	if err := fsetxattr(f, name, data, 0); err != nil {
		return &Error{"xattr.FSet", f.Name(), name, err}
	}
	return nil
}

// SetWithFlags associates name and data together as an attribute of path.
// Forwards the flags parameter to the syscall layer.
func SetWithFlags(path, name string, data []byte, flags int) error {
	if err := setxattr(path, name, data, flags); err != nil {
		return &Error{"xattr.SetWithFlags", path, name, err}
	}
	return nil
}

// LSetWithFlags is like SetWithFlags but does not follow a symlink at
// the end of the path.
func LSetWithFlags(path, name string, data []byte, flags int) error {
	if err := lsetxattr(path, name, data, flags); err != nil {
		return &Error{"xattr.LSetWithFlags", path, name, err}
	}
	return nil
}

// FSetWithFlags is like SetWithFlags but accepts a os.File instead of a file path.
func FSetWithFlags(f *os.File, name string, data []byte, flags int) error {
	if err := fsetxattr(f, name, data, flags); err != nil {
		return &Error{"xattr.FSetWithFlags", f.Name(), name, err}
	}
	return nil
}

// Remove removes the attribute associated with the given path.
func Remove(path, name string) error {
	if err := removexattr(path, name); err != nil {
		return &Error{"xattr.Remove", path, name, err}
	}
	return nil
}

// LRemove is like Remove but does not follow a symlink at the end of the
// path.
func LRemove(path, name string) error {
	if err := lremovexattr(path, name); err != nil {
		return &Error{"xattr.LRemove", path, name, err}
	}
	return nil
}

// FRemove is like Remove but accepts a os.File instead of a file path.
func FRemove(f *os.File, name string) error {
	if err := fremovexattr(f, name); err != nil {
		return &Error{"xattr.FRemove", f.Name(), name, err}
	}
	return nil
}

// List retrieves a list of names of extended attributes associated
// with the given path in the file system.
func List(path string) ([]string, error) {
	return list(path, func(data []byte) (int, error) {
		return listxattr(path, data)
	})
}

// LList is like List but does not follow a symlink at the end of the
// path.
func LList(path string) ([]string, error) {
	return list(path, func(data []byte) (int, error) {
		return llistxattr(path, data)
	})
}

// FList is like List but accepts a os.File instead of a file path.
func FList(f *os.File) ([]string, error) {
	return list(f.Name(), func(data []byte) (int, error) {
		return flistxattr(f, data)
	})
}

type listxattrFunc func(data []byte) (int, error)

// list contains the buffer allocation logic used by both List and LList.
func list(path string, listxattrFunc listxattrFunc) ([]string, error) {
	myname := "xattr.list"
	// find size.
	size, err := listxattrFunc(nil)
	if err != nil {
		return nil, &Error{myname, path, "", err}
	}
	if size > 0 {
		// `size + 1` because of ERANGE error when reading
		// from a SMB1 mount point (https://github.com/pkg/xattr/issues/16).
		buf := make([]byte, size+1)
		// Read into buffer of that size.
		read, err := listxattrFunc(buf)
		if err != nil {
			return nil, &Error{myname, path, "", err}
		}
		return stringsFromByteSlice(buf[:read]), nil
	}
	return []string{}, nil
}

// bytePtrFromSlice returns a pointer to array of bytes and a size.
func bytePtrFromSlice(data []byte) (ptr *byte, size int) {
	size = len(data)
	if size > 0 {
		ptr = &data[0]
	}
	return
}
