Merge remote-tracking branch 'origin/upstream'
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..2f1adf7
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,35 @@
+name: build
+on: [push]
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ go-version: [1.15, 1.16, 1.17, 1.18]
+ os: [ubuntu-latest, macos-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Set up Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: ${{ matrix.go-version }}
+
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Build
+ run: |
+ GOOS=freebsd go build
+ GOOS=windows go build
+ GOOS=openbsd go build
+ go build -v .
+
+ - name: Test
+ run: |
+ go vet
+ go test -v -race -coverprofile=coverage.txt -covermode=atomic
+
+ - name: After success
+ run: |
+ bash <(curl -s https://codecov.io/bash)
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d8b3265
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,26 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+.DS_Store
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+
+*.swp
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..99d2e9d
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2012 Dave Cheney. All rights reserved.
+Copyright (c) 2014 Kuba Podgórski. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..03160f9
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,15 @@
+name: "xattr"
+description:
+ "Extended attribute support for Go (linux + darwin + freebsd)"
+
+third_party {
+ identifier {
+ type: "Git"
+ value: "https://github.com/pkg/xattr"
+ primary_source: true
+ version: "v0.4.9"
+ }
+ version: "v0.4.9"
+ last_upgrade_date { year: 2024 month: 4 day: 22 }
+ license_type: NOTICE
+}
diff --git a/MODULE_LICENSE_BSD b/MODULE_LICENSE_BSD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_BSD
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..2e8f086
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1 @@
+include platform/system/core:main:/janitors/OWNERS
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0662c02
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+[](http://godoc.org/github.com/pkg/xattr)
+[](https://goreportcard.com/report/github.com/pkg/xattr)
+[](https://github.com/pkg/xattr/actions?query=workflow%3Abuild)
+[](https://codecov.io/gh/pkg/xattr)
+
+xattr
+=====
+Extended attribute support for Go (linux + darwin + freebsd + netbsd + solaris).
+
+"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." [See more...](https://en.wikipedia.org/wiki/Extended_file_attributes)
+
+`SetWithFlags` allows to additionally pass system flags to be forwarded to the underlying calls. FreeBSD and NetBSD do not support this and the parameter will be ignored.
+
+The `L` variants of all functions (`LGet/LSet/...`) are identical to `Get/Set/...` except that they
+do not reference a symlink that appears at the end of a path. See
+[GoDoc](http://godoc.org/github.com/pkg/xattr) for details.
+
+### Example
+```go
+ const path = "/tmp/myfile"
+ const prefix = "user."
+
+ if err := xattr.Set(path, prefix+"test", []byte("test-attr-value")); err != nil {
+ log.Fatal(err)
+ }
+
+ var list []string
+ if list, err = xattr.List(path); err != nil {
+ log.Fatal(err)
+ }
+
+ var data []byte
+ if data, err = xattr.Get(path, prefix+"test"); err != nil {
+ log.Fatal(err)
+ }
+
+ if err = xattr.Remove(path, prefix+"test"); err != nil {
+ log.Fatal(err)
+ }
+
+ // One can also specify the flags parameter to be passed to the OS.
+ if err := xattr.SetWithFlags(path, prefix+"test", []byte("test-attr-value"), xattr.XATTR_CREATE); err != nil {
+ log.Fatal(err)
+ }
+```
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..16df7bf
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module github.com/pkg/xattr
+
+go 1.14
+
+require golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c444bb9
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,4 @@
+golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
+golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
+golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/xattr.go b/xattr.go
new file mode 100644
index 0000000..8b2b5fe
--- /dev/null
+++ b/xattr.go
@@ -0,0 +1,257 @@
+/*
+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 at 64 KB. Unless the kernel is evil or buggy, we should never
+ // hit the limit.
+ 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.
+ // 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 || 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
+}
diff --git a/xattr_bsd.go b/xattr_bsd.go
new file mode 100644
index 0000000..f4a3f95
--- /dev/null
+++ b/xattr_bsd.go
@@ -0,0 +1,201 @@
+//go:build freebsd || netbsd
+// +build freebsd netbsd
+
+package xattr
+
+import (
+ "os"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ // XATTR_SUPPORTED will be true if the current platform is supported
+ XATTR_SUPPORTED = true
+
+ EXTATTR_NAMESPACE_USER = 1
+
+ // ENOATTR is not exported by the syscall package on Linux, because it is
+ // an alias for ENODATA. We export it here so it is available on all
+ // our supported platforms.
+ ENOATTR = syscall.ENOATTR
+)
+
+func getxattr(path string, name string, data []byte) (int, error) {
+ return sysGet(syscall.SYS_EXTATTR_GET_FILE, path, name, data)
+}
+
+func lgetxattr(path string, name string, data []byte) (int, error) {
+ return sysGet(syscall.SYS_EXTATTR_GET_LINK, path, name, data)
+}
+
+func fgetxattr(f *os.File, name string, data []byte) (int, error) {
+ return getxattr(f.Name(), name, data)
+}
+
+// sysGet is called by getxattr and lgetxattr with the appropriate syscall
+// number. This works because syscalls have the same signature and return
+// values.
+func sysGet(syscallNum uintptr, path string, name string, data []byte) (int, error) {
+ ptr, nbytes := bytePtrFromSlice(data)
+ /*
+ ssize_t extattr_get_file(
+ const char *path,
+ int attrnamespace,
+ const char *attrname,
+ void *data,
+ size_t nbytes);
+
+ ssize_t extattr_get_link(
+ const char *path,
+ int attrnamespace,
+ const char *attrname,
+ void *data,
+ size_t nbytes);
+ */
+ r0, _, err := syscall.Syscall6(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
+ EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(syscall.StringBytePtr(name))),
+ uintptr(unsafe.Pointer(ptr)), uintptr(nbytes), 0)
+ if err != syscall.Errno(0) {
+ return int(r0), err
+ }
+ return int(r0), nil
+}
+
+func setxattr(path string, name string, data []byte, flags int) error {
+ return sysSet(syscall.SYS_EXTATTR_SET_FILE, path, name, data)
+}
+
+func lsetxattr(path string, name string, data []byte, flags int) error {
+ return sysSet(syscall.SYS_EXTATTR_SET_LINK, path, name, data)
+}
+
+func fsetxattr(f *os.File, name string, data []byte, flags int) error {
+ return setxattr(f.Name(), name, data, flags)
+}
+
+// sysSet is called by setxattr and lsetxattr with the appropriate syscall
+// number. This works because syscalls have the same signature and return
+// values.
+func sysSet(syscallNum uintptr, path string, name string, data []byte) error {
+ ptr, nbytes := bytePtrFromSlice(data)
+ /*
+ ssize_t extattr_set_file(
+ const char *path,
+ int attrnamespace,
+ const char *attrname,
+ const void *data,
+ size_t nbytes
+ );
+
+ ssize_t extattr_set_link(
+ const char *path,
+ int attrnamespace,
+ const char *attrname,
+ const void *data,
+ size_t nbytes
+ );
+ */
+ r0, _, err := syscall.Syscall6(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
+ EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(syscall.StringBytePtr(name))),
+ uintptr(unsafe.Pointer(ptr)), uintptr(nbytes), 0)
+ if err != syscall.Errno(0) {
+ return err
+ }
+ if int(r0) != nbytes {
+ return syscall.E2BIG
+ }
+ return nil
+}
+
+func removexattr(path string, name string) error {
+ return sysRemove(syscall.SYS_EXTATTR_DELETE_FILE, path, name)
+}
+
+func lremovexattr(path string, name string) error {
+ return sysRemove(syscall.SYS_EXTATTR_DELETE_LINK, path, name)
+}
+
+func fremovexattr(f *os.File, name string) error {
+ return removexattr(f.Name(), name)
+}
+
+// sysSet is called by removexattr and lremovexattr with the appropriate syscall
+// number. This works because syscalls have the same signature and return
+// values.
+func sysRemove(syscallNum uintptr, path string, name string) error {
+ /*
+ int extattr_delete_file(
+ const char *path,
+ int attrnamespace,
+ const char *attrname
+ );
+
+ int extattr_delete_link(
+ const char *path,
+ int attrnamespace,
+ const char *attrname
+ );
+ */
+ _, _, err := syscall.Syscall(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
+ EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(syscall.StringBytePtr(name))),
+ )
+ if err != syscall.Errno(0) {
+ return err
+ }
+ return nil
+}
+
+func listxattr(path string, data []byte) (int, error) {
+ return sysList(syscall.SYS_EXTATTR_LIST_FILE, path, data)
+}
+
+func llistxattr(path string, data []byte) (int, error) {
+ return sysList(syscall.SYS_EXTATTR_LIST_LINK, path, data)
+}
+
+func flistxattr(f *os.File, data []byte) (int, error) {
+ return listxattr(f.Name(), data)
+}
+
+// sysSet is called by listxattr and llistxattr with the appropriate syscall
+// number. This works because syscalls have the same signature and return
+// values.
+func sysList(syscallNum uintptr, path string, data []byte) (int, error) {
+ ptr, nbytes := bytePtrFromSlice(data)
+ /*
+ ssize_t extattr_list_file(
+ const char *path,
+ int attrnamespace,
+ void *data,
+ size_t nbytes
+ );
+
+ ssize_t extattr_list_link(
+ const char *path,
+ int attrnamespace,
+ void *data,
+ size_t nbytes
+ );
+ */
+ r0, _, err := syscall.Syscall6(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
+ EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(ptr)), uintptr(nbytes), 0, 0)
+ if err != syscall.Errno(0) {
+ return int(r0), err
+ }
+ return int(r0), nil
+}
+
+// stringsFromByteSlice converts a sequence of attributes to a []string.
+// On FreeBSD, each entry consists of a single byte containing the length
+// of the attribute name, followed by the attribute name.
+// The name is _not_ terminated by NULL.
+func stringsFromByteSlice(buf []byte) (result []string) {
+ index := 0
+ for index < len(buf) {
+ next := index + 1 + int(buf[index])
+ result = append(result, string(buf[index+1:next]))
+ index = next
+ }
+ return
+}
diff --git a/xattr_darwin.go b/xattr_darwin.go
new file mode 100644
index 0000000..ee7a501
--- /dev/null
+++ b/xattr_darwin.go
@@ -0,0 +1,90 @@
+//go:build darwin
+// +build darwin
+
+package xattr
+
+import (
+ "os"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+// See https://opensource.apple.com/source/xnu/xnu-1504.15.3/bsd/sys/xattr.h.auto.html
+const (
+ // XATTR_SUPPORTED will be true if the current platform is supported
+ XATTR_SUPPORTED = true
+
+ XATTR_NOFOLLOW = 0x0001
+ XATTR_CREATE = 0x0002
+ XATTR_REPLACE = 0x0004
+ XATTR_NOSECURITY = 0x0008
+ XATTR_NODEFAULT = 0x0010
+ XATTR_SHOWCOMPRESSION = 0x0020
+
+ // ENOATTR is not exported by the syscall package on Linux, because it is
+ // an alias for ENODATA. We export it here so it is available on all
+ // our supported platforms.
+ ENOATTR = syscall.ENOATTR
+)
+
+func getxattr(path string, name string, data []byte) (int, error) {
+ return unix.Getxattr(path, name, data)
+}
+
+func lgetxattr(path string, name string, data []byte) (int, error) {
+ return unix.Lgetxattr(path, name, data)
+}
+
+func fgetxattr(f *os.File, name string, data []byte) (int, error) {
+ return getxattr(f.Name(), name, data)
+}
+
+func setxattr(path string, name string, data []byte, flags int) error {
+ return unix.Setxattr(path, name, data, flags)
+}
+
+func lsetxattr(path string, name string, data []byte, flags int) error {
+ return unix.Lsetxattr(path, name, data, flags)
+}
+
+func fsetxattr(f *os.File, name string, data []byte, flags int) error {
+ return setxattr(f.Name(), name, data, flags)
+}
+
+func removexattr(path string, name string) error {
+ return unix.Removexattr(path, name)
+}
+
+func lremovexattr(path string, name string) error {
+ return unix.Lremovexattr(path, name)
+}
+
+func fremovexattr(f *os.File, name string) error {
+ return removexattr(f.Name(), name)
+}
+
+func listxattr(path string, data []byte) (int, error) {
+ return unix.Listxattr(path, data)
+}
+
+func llistxattr(path string, data []byte) (int, error) {
+ return unix.Llistxattr(path, data)
+}
+
+func flistxattr(f *os.File, data []byte) (int, error) {
+ return listxattr(f.Name(), data)
+}
+
+// stringsFromByteSlice converts a sequence of attributes to a []string.
+// On Darwin and Linux, 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
+}
diff --git a/xattr_flags_test.go b/xattr_flags_test.go
new file mode 100644
index 0000000..96dd428
--- /dev/null
+++ b/xattr_flags_test.go
@@ -0,0 +1,39 @@
+//go:build linux || darwin || solaris
+// +build linux darwin solaris
+
+package xattr
+
+import (
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+func TestFlags(t *testing.T) {
+ tmp, err := ioutil.TempFile("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+
+ err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), 0)
+ checkIfError(t, err)
+
+ err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), XATTR_CREATE)
+ if err == nil {
+ t.Fatalf("XATTR_CREATE should have failed because the xattr already exists")
+ }
+ t.Log(err)
+
+ err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), XATTR_REPLACE)
+ checkIfError(t, err)
+
+ err = Remove(tmp.Name(), UserPrefix+"flags-test")
+ checkIfError(t, err)
+
+ err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), XATTR_REPLACE)
+ if err == nil {
+ t.Fatalf("XATTR_REPLACE should have failed because there is nothing to replace")
+ }
+ t.Log(err)
+}
diff --git a/xattr_linux.go b/xattr_linux.go
new file mode 100644
index 0000000..879085e
--- /dev/null
+++ b/xattr_linux.go
@@ -0,0 +1,142 @@
+//go:build linux
+// +build linux
+
+package xattr
+
+import (
+ "os"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+const (
+ // XATTR_SUPPORTED will be true if the current platform is supported
+ XATTR_SUPPORTED = true
+
+ XATTR_CREATE = unix.XATTR_CREATE
+ XATTR_REPLACE = unix.XATTR_REPLACE
+
+ // ENOATTR is not exported by the syscall package on Linux, because it is
+ // an alias for ENODATA. We export it here so it is available on all
+ // our supported platforms.
+ ENOATTR = syscall.ENODATA
+)
+
+// On Linux, FUSE and CIFS filesystems can return EINTR for interrupted system
+// calls. This function works around this by retrying system calls until they
+// stop returning EINTR.
+//
+// See https://github.com/golang/go/commit/6b420169d798c7ebe733487b56ea5c3fa4aab5ce.
+func ignoringEINTR(fn func() error) (err error) {
+ for {
+ err = fn()
+ if err != unix.EINTR {
+ break
+ }
+ }
+ return err
+}
+
+func getxattr(path string, name string, data []byte) (int, error) {
+ var r int
+ err := ignoringEINTR(func() (err error) {
+ r, err = unix.Getxattr(path, name, data)
+ return err
+ })
+ return r, err
+}
+
+func lgetxattr(path string, name string, data []byte) (int, error) {
+ var r int
+ err := ignoringEINTR(func() (err error) {
+ r, err = unix.Lgetxattr(path, name, data)
+ return err
+ })
+ return r, err
+}
+
+func fgetxattr(f *os.File, name string, data []byte) (int, error) {
+ var r int
+ err := ignoringEINTR(func() (err error) {
+ r, err = unix.Fgetxattr(int(f.Fd()), name, data)
+ return err
+ })
+ return r, err
+}
+
+func setxattr(path string, name string, data []byte, flags int) error {
+ return ignoringEINTR(func() (err error) {
+ return unix.Setxattr(path, name, data, flags)
+ })
+}
+
+func lsetxattr(path string, name string, data []byte, flags int) error {
+ return ignoringEINTR(func() (err error) {
+ return unix.Lsetxattr(path, name, data, flags)
+ })
+}
+
+func fsetxattr(f *os.File, name string, data []byte, flags int) error {
+ return ignoringEINTR(func() (err error) {
+ return unix.Fsetxattr(int(f.Fd()), name, data, flags)
+ })
+}
+
+func removexattr(path string, name string) error {
+ return ignoringEINTR(func() (err error) {
+ return unix.Removexattr(path, name)
+ })
+}
+
+func lremovexattr(path string, name string) error {
+ return ignoringEINTR(func() (err error) {
+ return unix.Lremovexattr(path, name)
+ })
+}
+
+func fremovexattr(f *os.File, name string) error {
+ return ignoringEINTR(func() (err error) {
+ return unix.Fremovexattr(int(f.Fd()), name)
+ })
+}
+
+func listxattr(path string, data []byte) (int, error) {
+ var r int
+ err := ignoringEINTR(func() (err error) {
+ r, err = unix.Listxattr(path, data)
+ return err
+ })
+ return r, err
+}
+
+func llistxattr(path string, data []byte) (int, error) {
+ var r int
+ err := ignoringEINTR(func() (err error) {
+ r, err = unix.Llistxattr(path, data)
+ return err
+ })
+ return r, err
+}
+
+func flistxattr(f *os.File, data []byte) (int, error) {
+ var r int
+ err := ignoringEINTR(func() (err error) {
+ r, err = unix.Flistxattr(int(f.Fd()), data)
+ return err
+ })
+ return r, err
+}
+
+// stringsFromByteSlice converts a sequence of attributes to a []string.
+// On Darwin and Linux, 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
+}
diff --git a/xattr_linux_test.go b/xattr_linux_test.go
new file mode 100644
index 0000000..7986511
--- /dev/null
+++ b/xattr_linux_test.go
@@ -0,0 +1,21 @@
+package xattr
+
+import (
+ "syscall"
+ "testing"
+)
+
+func TestIgnoringEINTR(t *testing.T) {
+ eintrs := 100
+ err := ignoringEINTR(func() error {
+ if eintrs == 0 {
+ return nil
+ }
+ eintrs--
+ return syscall.EINTR
+ })
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/xattr_solaris.go b/xattr_solaris.go
new file mode 100644
index 0000000..8d65b8d
--- /dev/null
+++ b/xattr_solaris.go
@@ -0,0 +1,165 @@
+//go:build solaris
+// +build solaris
+
+package xattr
+
+import (
+ "os"
+ "syscall"
+
+ "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 exported by the syscall package on Linux, because it is
+ // an alias for ENODATA. We export it here so it is available on all
+ // our supported platforms.
+ ENOATTR = syscall.ENODATA
+)
+
+func getxattr(path string, name string, data []byte) (int, error) {
+ f, err := os.OpenFile(path, os.O_RDONLY, 0)
+ 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 := os.OpenFile(path, os.O_RDONLY, 0)
+ 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 {
+ fd, err := unix.Open(path, unix.O_RDONLY|unix.O_XATTR, 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 := os.OpenFile(path, os.O_RDONLY, 0)
+ 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 {
+ return 0, unix.ENOTSUP
+ }
+ 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
+}
+
+// stringsFromByteSlice converts a sequence of attributes to a []string.
+// On Darwin and Linux, 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
+}
diff --git a/xattr_test.go b/xattr_test.go
new file mode 100644
index 0000000..b6b2048
--- /dev/null
+++ b/xattr_test.go
@@ -0,0 +1,340 @@
+//go:build linux || darwin || freebsd || netbsd || solaris
+// +build linux darwin freebsd netbsd solaris
+
+package xattr
+
+import (
+ "bytes"
+ "io/ioutil"
+ "log"
+ "os"
+ "runtime"
+ "syscall"
+ "testing"
+)
+
+const UserPrefix = "user."
+
+type funcFamily struct {
+ familyName string
+ get func(path, name string) ([]byte, error)
+ set func(path, name string, data []byte) error
+ setWithFlags func(path, name string, data []byte, flags int) error
+ remove func(path, name string) error
+ list func(path string) ([]string, error)
+}
+
+// Test Get, Set, List, Remove on a regular file
+func TestRegularFile(t *testing.T) {
+ families := []funcFamily{
+ {
+ familyName: "Get and friends",
+ get: Get,
+ set: Set,
+ setWithFlags: SetWithFlags,
+ remove: Remove,
+ list: List,
+ },
+ {
+ familyName: "LGet and friends",
+ get: LGet,
+ set: LSet,
+ setWithFlags: LSetWithFlags,
+ remove: LRemove,
+ list: LList,
+ },
+ {
+ familyName: "FGet and friends",
+ get: wrapFGet,
+ set: wrapFSet,
+ setWithFlags: wrapFSetWithFlags,
+ remove: wrapFRemove,
+ list: wrapFList,
+ },
+ }
+ for _, ff := range families {
+ t.Run(ff.familyName, func(t *testing.T) {
+ t.Logf("Testing %q on a regular file", ff.familyName)
+ testRegularFile(t, ff)
+ })
+ }
+}
+
+// testRegularFile is called with the "Get and friends" and the
+// "LGet and friends" function family. Both families should behave
+// the same on a regular file.
+func testRegularFile(t *testing.T, ff funcFamily) {
+ tmp, err := ioutil.TempFile("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+
+ xName := UserPrefix + "test"
+ xVal := []byte("test-attr-value")
+
+ // Test that SetWithFlags succeeds and that the xattr shows up in List()
+ err = ff.setWithFlags(tmp.Name(), xName, xVal, 0)
+ checkIfError(t, err)
+ list, err := ff.list(tmp.Name())
+ checkIfError(t, err)
+ found := false
+ for _, name := range list {
+ if name == xName {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatalf("List/LList did not return test attribute: %q", list)
+ }
+ err = ff.remove(tmp.Name(), xName)
+ checkIfError(t, err)
+
+ // Test that Set succeeds and that the the xattr shows up in List()
+ err = ff.set(tmp.Name(), xName, xVal)
+ checkIfError(t, err)
+ list, err = ff.list(tmp.Name())
+ checkIfError(t, err)
+ found = false
+ for _, name := range list {
+ if name == xName {
+ found = true
+ }
+ }
+ if !found {
+ t.Fatalf("List/LList did not return test attribute: %q", list)
+ }
+
+ var data []byte
+ data, err = ff.get(tmp.Name(), xName)
+ checkIfError(t, err)
+
+ value := string(data)
+ t.Log(value)
+ if string(xVal) != value {
+ t.Fail()
+ }
+
+ err = ff.remove(tmp.Name(), xName)
+ checkIfError(t, err)
+}
+
+// Test that setting an xattr with an empty value works.
+func TestNoData(t *testing.T) {
+ tmp, err := ioutil.TempFile("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+
+ err = Set(tmp.Name(), UserPrefix+"test", []byte{})
+ checkIfError(t, err)
+
+ list, err := List(tmp.Name())
+ checkIfError(t, err)
+
+ found := false
+ for _, name := range list {
+ if name == UserPrefix+"test" {
+ found = true
+ }
+ }
+
+ if !found {
+ t.Fatal("Listxattr did not return test attribute")
+ }
+}
+
+// Test that Get/LGet, Set/LSet etc operate as expected on symlinks. The
+// functions should behave differently when operating on a symlink.
+func TestSymlink(t *testing.T) {
+ if runtime.GOOS == "solaris" || runtime.GOOS == "illumos" {
+ t.Skipf("extended attributes aren't supported for symlinks on %s", runtime.GOOS)
+ }
+ dir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ s := dir + "/symlink1"
+ err = os.Symlink(dir+"/some/nonexistent/path", s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ xName := UserPrefix + "TestSymlink"
+ xVal := []byte("test")
+
+ // Test Set/LSet
+ if err := Set(s, xName, xVal); err == nil {
+ t.Error("Set on a broken symlink should fail, but did not")
+ }
+ err = LSet(s, xName, xVal)
+ errno := unpackSysErr(err)
+ setOk := true
+ if runtime.GOOS == "linux" && errno == syscall.EPERM {
+ // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/xattr.c?h=v4.17-rc5#n122 :
+ // In the user.* namespace, only regular files and directories can have
+ // extended attributes.
+ t.Log("got EPERM, adjusting test scope")
+ setOk = false
+ } else {
+ checkIfError(t, err)
+ }
+
+ // Test List/LList
+ _, err = List(s)
+ errno = unpackSysErr(err)
+ if errno != syscall.ENOENT {
+ t.Errorf("List() on a broken symlink should fail with ENOENT, got %q", errno)
+ }
+ data, err := LList(s)
+ checkIfError(t, err)
+ if setOk {
+ found := false
+ for _, n := range data {
+ if n == xName {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("xattr %q did not show up in Llist output: %q", xName, data)
+ }
+ }
+
+ // Test Get/LGet
+ _, err = Get(s, xName)
+ errno = unpackSysErr(err)
+ if errno != syscall.ENOENT {
+ t.Errorf("Get() on a broken symlink should fail with ENOENT, got %q", errno)
+ }
+ val, err := LGet(s, xName)
+ if setOk {
+ checkIfError(t, err)
+ if !bytes.Equal(xVal, val) {
+ t.Errorf("wrong xattr value: want=%q have=%q", xVal, val)
+ }
+ } else {
+ errno = unpackSysErr(err)
+ if errno != ENOATTR {
+ t.Errorf("expected ENOATTR, got %q", errno)
+ }
+ }
+
+ // Test Remove/Lremove
+ err = Remove(s, xName)
+ errno = unpackSysErr(err)
+ if errno != syscall.ENOENT {
+ t.Errorf("Remove() on a broken symlink should fail with ENOENT, got %q", errno)
+ }
+ err = LRemove(s, xName)
+ if setOk {
+ checkIfError(t, err)
+ } else {
+ errno = unpackSysErr(err)
+ if errno != syscall.EPERM {
+ t.Errorf("expected EPERM, got %q", errno)
+ }
+ }
+}
+
+// Verify that Get() handles values larger than the default buffer size (1 KB)
+func TestLargeVal(t *testing.T) {
+ tmp, err := ioutil.TempFile("", "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+ path := tmp.Name()
+
+ key := UserPrefix + "TestERANGE"
+ // On ext4, key + value length must be <= 4096. Use 4000 so we can test
+ // reliably on ext4.
+ val := bytes.Repeat([]byte("z"), 4000)
+ err = Set(path, key, val)
+ checkIfError(t, err)
+
+ val2, err := Get(path, key)
+ checkIfError(t, err)
+ if !bytes.Equal(val, val2) {
+ t.Errorf("wrong result from Get: want=%s have=%s", string(val), string(val2))
+ }
+}
+
+// checkIfError calls t.Skip() if the underlying syscall.Errno is
+// ENOTSUP or EOPNOTSUPP. It calls t.Fatal() on any other non-zero error.
+func checkIfError(t *testing.T, err error) {
+ errno := unpackSysErr(err)
+ if errno == syscall.Errno(0) {
+ return
+ }
+ // check if filesystem supports extended attributes
+ if errno == syscall.Errno(syscall.ENOTSUP) || errno == syscall.Errno(syscall.EOPNOTSUPP) {
+ t.Skip("Skipping test - filesystem does not support extended attributes")
+ } else {
+ t.Fatal(err)
+ }
+}
+
+// unpackSysErr unpacks the underlying syscall.Errno from an error value
+// returned by Get/Set/...
+func unpackSysErr(err error) syscall.Errno {
+ if err == nil {
+ return syscall.Errno(0)
+ }
+ err2, ok := err.(*Error)
+ if !ok {
+ log.Panicf("cannot unpack err=%#v", err)
+ }
+ err3, ok := err2.Err.(syscall.Errno)
+ if !ok {
+ log.Panicf("cannot unpack err2=%#v", err2)
+ }
+ return err3
+}
+
+// wrappers to adapt "F" variants to the test
+
+func wrapFGet(path, name string) ([]byte, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return FGet(f, name)
+}
+
+func wrapFSet(path, name string, data []byte) error {
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ return FSet(f, name, data)
+}
+
+func wrapFSetWithFlags(path, name string, data []byte, flags int) error {
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ return FSetWithFlags(f, name, data, flags)
+}
+
+func wrapFRemove(path, name string) error {
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ return FRemove(f, name)
+}
+
+func wrapFList(path string) ([]string, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return FList(f)
+}
diff --git a/xattr_unsupported.go b/xattr_unsupported.go
new file mode 100644
index 0000000..8886fbd
--- /dev/null
+++ b/xattr_unsupported.go
@@ -0,0 +1,70 @@
+//go:build !linux && !freebsd && !netbsd && !darwin && !solaris
+// +build !linux,!freebsd,!netbsd,!darwin,!solaris
+
+package xattr
+
+import (
+ "os"
+ "syscall"
+)
+
+const (
+ // We need to use the default for non supported operating systems
+ ENOATTR = syscall.Errno(0x59)
+)
+
+// XATTR_SUPPORTED will be true if the current platform is supported
+const XATTR_SUPPORTED = false
+
+func getxattr(path string, name string, data []byte) (int, error) {
+ return 0, nil
+}
+
+func lgetxattr(path string, name string, data []byte) (int, error) {
+ return 0, nil
+}
+
+func fgetxattr(f *os.File, name string, data []byte) (int, error) {
+ return 0, nil
+}
+
+func setxattr(path string, name string, data []byte, flags int) error {
+ return nil
+}
+
+func lsetxattr(path string, name string, data []byte, flags int) error {
+ return nil
+}
+
+func fsetxattr(f *os.File, name string, data []byte, flags int) error {
+ return nil
+}
+
+func removexattr(path string, name string) error {
+ return nil
+}
+
+func lremovexattr(path string, name string) error {
+ return nil
+}
+
+func fremovexattr(f *os.File, name string) error {
+ return nil
+}
+
+func listxattr(path string, data []byte) (int, error) {
+ return 0, nil
+}
+
+func llistxattr(path string, data []byte) (int, error) {
+ return 0, nil
+}
+
+func flistxattr(f *os.File, data []byte) (int, error) {
+ return 0, nil
+}
+
+// dummy
+func stringsFromByteSlice(buf []byte) (result []string) {
+ return []string{}
+}