├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── xattr.go ├── xattr_bsd.go ├── xattr_darwin.go ├── xattr_flags_test.go ├── xattr_linux.go ├── xattr_linux_test.go ├── xattr_solaris.go ├── xattr_test.go └── xattr_unsupported.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | strategy: 7 | matrix: 8 | go-version: ['1.16', '1.17', '1.18', '1.19', '1.20', '1.21', '1.22'] 9 | os: [ubuntu-latest, macos-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: ${{ matrix.go-version }} 16 | 17 | - name: Checkout code 18 | uses: actions/checkout@v2 19 | 20 | - name: Build 21 | run: | 22 | GOOS=freebsd go build 23 | GOOS=openbsd go build 24 | go build -v . 25 | 26 | - name: Test 27 | run: | 28 | go vet 29 | go test -v -race -coverprofile=coverage.txt -covermode=atomic 30 | 31 | - name: After success 32 | run: | 33 | bash <(curl -s https://codecov.io/bash) 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | .DS_Store 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | 26 | *.swp 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Dave Cheney. All rights reserved. 2 | Copyright (c) 2014 Kuba Podgórski. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 18 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 19 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 20 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 21 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/pkg/xattr?status.svg)](http://godoc.org/github.com/pkg/xattr) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/pkg/xattr)](https://goreportcard.com/report/github.com/pkg/xattr) 3 | [![Build Status](https://github.com/pkg/xattr/workflows/build/badge.svg)](https://github.com/pkg/xattr/actions?query=workflow%3Abuild) 4 | [![Codecov](https://codecov.io/gh/pkg/xattr/branch/master/graph/badge.svg)](https://codecov.io/gh/pkg/xattr) 5 | 6 | xattr 7 | ===== 8 | Extended attribute support for Go (linux + darwin + freebsd + netbsd + solaris). 9 | 10 | "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) 11 | 12 | `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. 13 | 14 | The `L` variants of all functions (`LGet/LSet/...`) are identical to `Get/Set/...` except that they 15 | do not reference a symlink that appears at the end of a path. See 16 | [GoDoc](http://godoc.org/github.com/pkg/xattr) for details. 17 | 18 | ### Example 19 | ```go 20 | const path = "/tmp/myfile" 21 | const prefix = "user." 22 | 23 | if err := xattr.Set(path, prefix+"test", []byte("test-attr-value")); err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | var list []string 28 | if list, err = xattr.List(path); err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | var data []byte 33 | if data, err = xattr.Get(path, prefix+"test"); err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | if err = xattr.Remove(path, prefix+"test"); err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | // One can also specify the flags parameter to be passed to the OS. 42 | if err := xattr.SetWithFlags(path, prefix+"test", []byte("test-attr-value"), xattr.XATTR_CREATE); err != nil { 43 | log.Fatal(err) 44 | } 45 | ``` 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pkg/xattr 2 | 3 | go 1.14 4 | 5 | require golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck= 2 | golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 3 | golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= 4 | golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | -------------------------------------------------------------------------------- /xattr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package xattr provides support for extended attributes on linux, darwin and freebsd. 3 | Extended attributes are name:value pairs associated permanently with files and directories, 4 | similar to the environment strings associated with a process. 5 | An attribute may be defined or undefined. If it is defined, its value may be empty or non-empty. 6 | More details you can find here: https://en.wikipedia.org/wiki/Extended_file_attributes . 7 | 8 | All functions are provided in triples: Get/LGet/FGet, Set/LSet/FSet etc. The "L" 9 | variant will not follow a symlink at the end of the path, and "F" variant accepts 10 | a file descriptor instead of a path. 11 | 12 | Example for "L" variant, assuming path is "/symlink1/symlink2", where both components are 13 | symlinks: 14 | Get will follow "symlink1" and "symlink2" and operate on the target of 15 | "symlink2". LGet will follow "symlink1" but operate directly on "symlink2". 16 | */ 17 | package xattr 18 | 19 | import ( 20 | "os" 21 | "syscall" 22 | ) 23 | 24 | // Error records an error and the operation, file path and attribute that caused it. 25 | type Error struct { 26 | Op string 27 | Path string 28 | Name string 29 | Err error 30 | } 31 | 32 | func (e *Error) Unwrap() error { return e.Err } 33 | 34 | func (e *Error) Error() (errstr string) { 35 | if e.Op != "" { 36 | errstr += e.Op 37 | } 38 | if e.Path != "" { 39 | if errstr != "" { 40 | errstr += " " 41 | } 42 | errstr += e.Path 43 | } 44 | if e.Name != "" { 45 | if errstr != "" { 46 | errstr += " " 47 | } 48 | errstr += e.Name 49 | } 50 | if e.Err != nil { 51 | if errstr != "" { 52 | errstr += ": " 53 | } 54 | errstr += e.Err.Error() 55 | } 56 | return 57 | } 58 | 59 | // Get retrieves extended attribute data associated with path. It will follow 60 | // all symlinks along the path. 61 | func Get(path, name string) ([]byte, error) { 62 | return get(path, name, func(name string, data []byte) (int, error) { 63 | return getxattr(path, name, data) 64 | }) 65 | } 66 | 67 | // LGet is like Get but does not follow a symlink at the end of the path. 68 | func LGet(path, name string) ([]byte, error) { 69 | return get(path, name, func(name string, data []byte) (int, error) { 70 | return lgetxattr(path, name, data) 71 | }) 72 | } 73 | 74 | // FGet is like Get but accepts a os.File instead of a file path. 75 | func FGet(f *os.File, name string) ([]byte, error) { 76 | return get(f.Name(), name, func(name string, data []byte) (int, error) { 77 | return fgetxattr(f, name, data) 78 | }) 79 | } 80 | 81 | type getxattrFunc func(name string, data []byte) (int, error) 82 | 83 | // get contains the buffer allocation logic used by both Get and LGet. 84 | func get(path string, name string, getxattrFunc getxattrFunc) ([]byte, error) { 85 | const ( 86 | // Start with a 1 KB buffer for the xattr value 87 | initialBufSize = 1024 88 | 89 | // The theoretical maximum xattr value size on MacOS is 64 MB. On Linux it's 90 | // much smaller: documented at 64 KB. However, at least on TrueNAS SCALE, a 91 | // Debian-based Linux distro, it can be larger. 92 | maxBufSize = 64 * 1024 * 1024 93 | 94 | // Function name as reported in error messages 95 | myname = "xattr.get" 96 | ) 97 | 98 | size := initialBufSize 99 | for { 100 | data := make([]byte, size) 101 | read, err := getxattrFunc(name, data) 102 | 103 | // If the buffer was too small to fit the value, Linux and MacOS react 104 | // differently: 105 | // Linux: returns an ERANGE error and "-1" bytes. However, the TrueNAS 106 | // SCALE distro sometimes returns E2BIG. 107 | // MacOS: truncates the value and returns "size" bytes. If the value 108 | // happens to be exactly as big as the buffer, we cannot know if it was 109 | // truncated, and we retry with a bigger buffer. Contrary to documentation, 110 | // MacOS never seems to return ERANGE! 111 | // To keep the code simple, we always check both conditions, and sometimes 112 | // double the buffer size without it being strictly necessary. 113 | if err == syscall.ERANGE || err == syscall.E2BIG || read == size { 114 | // The buffer was too small. Try again. 115 | size <<= 1 116 | if size >= maxBufSize { 117 | return nil, &Error{myname, path, name, syscall.EOVERFLOW} 118 | } 119 | continue 120 | } 121 | if err != nil { 122 | return nil, &Error{myname, path, name, err} 123 | } 124 | return data[:read], nil 125 | } 126 | } 127 | 128 | // Set associates name and data together as an attribute of path. 129 | func Set(path, name string, data []byte) error { 130 | if err := setxattr(path, name, data, 0); err != nil { 131 | return &Error{"xattr.Set", path, name, err} 132 | } 133 | return nil 134 | } 135 | 136 | // LSet is like Set but does not follow a symlink at 137 | // the end of the path. 138 | func LSet(path, name string, data []byte) error { 139 | if err := lsetxattr(path, name, data, 0); err != nil { 140 | return &Error{"xattr.LSet", path, name, err} 141 | } 142 | return nil 143 | } 144 | 145 | // FSet is like Set but accepts a os.File instead of a file path. 146 | func FSet(f *os.File, name string, data []byte) error { 147 | if err := fsetxattr(f, name, data, 0); err != nil { 148 | return &Error{"xattr.FSet", f.Name(), name, err} 149 | } 150 | return nil 151 | } 152 | 153 | // SetWithFlags associates name and data together as an attribute of path. 154 | // Forwards the flags parameter to the syscall layer. 155 | func SetWithFlags(path, name string, data []byte, flags int) error { 156 | if err := setxattr(path, name, data, flags); err != nil { 157 | return &Error{"xattr.SetWithFlags", path, name, err} 158 | } 159 | return nil 160 | } 161 | 162 | // LSetWithFlags is like SetWithFlags but does not follow a symlink at 163 | // the end of the path. 164 | func LSetWithFlags(path, name string, data []byte, flags int) error { 165 | if err := lsetxattr(path, name, data, flags); err != nil { 166 | return &Error{"xattr.LSetWithFlags", path, name, err} 167 | } 168 | return nil 169 | } 170 | 171 | // FSetWithFlags is like SetWithFlags but accepts a os.File instead of a file path. 172 | func FSetWithFlags(f *os.File, name string, data []byte, flags int) error { 173 | if err := fsetxattr(f, name, data, flags); err != nil { 174 | return &Error{"xattr.FSetWithFlags", f.Name(), name, err} 175 | } 176 | return nil 177 | } 178 | 179 | // Remove removes the attribute associated with the given path. 180 | func Remove(path, name string) error { 181 | if err := removexattr(path, name); err != nil { 182 | return &Error{"xattr.Remove", path, name, err} 183 | } 184 | return nil 185 | } 186 | 187 | // LRemove is like Remove but does not follow a symlink at the end of the 188 | // path. 189 | func LRemove(path, name string) error { 190 | if err := lremovexattr(path, name); err != nil { 191 | return &Error{"xattr.LRemove", path, name, err} 192 | } 193 | return nil 194 | } 195 | 196 | // FRemove is like Remove but accepts a os.File instead of a file path. 197 | func FRemove(f *os.File, name string) error { 198 | if err := fremovexattr(f, name); err != nil { 199 | return &Error{"xattr.FRemove", f.Name(), name, err} 200 | } 201 | return nil 202 | } 203 | 204 | // List retrieves a list of names of extended attributes associated 205 | // with the given path in the file system. 206 | func List(path string) ([]string, error) { 207 | return list(path, func(data []byte) (int, error) { 208 | return listxattr(path, data) 209 | }) 210 | } 211 | 212 | // LList is like List but does not follow a symlink at the end of the 213 | // path. 214 | func LList(path string) ([]string, error) { 215 | return list(path, func(data []byte) (int, error) { 216 | return llistxattr(path, data) 217 | }) 218 | } 219 | 220 | // FList is like List but accepts a os.File instead of a file path. 221 | func FList(f *os.File) ([]string, error) { 222 | return list(f.Name(), func(data []byte) (int, error) { 223 | return flistxattr(f, data) 224 | }) 225 | } 226 | 227 | type listxattrFunc func(data []byte) (int, error) 228 | 229 | // list contains the buffer allocation logic used by both List and LList. 230 | func list(path string, listxattrFunc listxattrFunc) ([]string, error) { 231 | myname := "xattr.list" 232 | // find size. 233 | size, err := listxattrFunc(nil) 234 | if err != nil { 235 | return nil, &Error{myname, path, "", err} 236 | } 237 | if size > 0 { 238 | // `size + 1` because of ERANGE error when reading 239 | // from a SMB1 mount point (https://github.com/pkg/xattr/issues/16). 240 | buf := make([]byte, size+1) 241 | // Read into buffer of that size. 242 | read, err := listxattrFunc(buf) 243 | if err != nil { 244 | return nil, &Error{myname, path, "", err} 245 | } 246 | return stringsFromByteSlice(buf[:read]), nil 247 | } 248 | return []string{}, nil 249 | } 250 | 251 | // bytePtrFromSlice returns a pointer to array of bytes and a size. 252 | func bytePtrFromSlice(data []byte) (ptr *byte, size int) { 253 | size = len(data) 254 | if size > 0 { 255 | ptr = &data[0] 256 | } 257 | return 258 | } 259 | -------------------------------------------------------------------------------- /xattr_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build freebsd || netbsd 2 | // +build freebsd netbsd 3 | 4 | package xattr 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | "unsafe" 10 | ) 11 | 12 | const ( 13 | // XATTR_SUPPORTED will be true if the current platform is supported 14 | XATTR_SUPPORTED = true 15 | 16 | EXTATTR_NAMESPACE_USER = 1 17 | 18 | // ENOATTR is not exported by the syscall package on Linux, because it is 19 | // an alias for ENODATA. We export it here so it is available on all 20 | // our supported platforms. 21 | ENOATTR = syscall.ENOATTR 22 | ) 23 | 24 | func getxattr(path string, name string, data []byte) (int, error) { 25 | return sysGet(syscall.SYS_EXTATTR_GET_FILE, path, name, data) 26 | } 27 | 28 | func lgetxattr(path string, name string, data []byte) (int, error) { 29 | return sysGet(syscall.SYS_EXTATTR_GET_LINK, path, name, data) 30 | } 31 | 32 | func fgetxattr(f *os.File, name string, data []byte) (int, error) { 33 | return getxattr(f.Name(), name, data) 34 | } 35 | 36 | // sysGet is called by getxattr and lgetxattr with the appropriate syscall 37 | // number. This works because syscalls have the same signature and return 38 | // values. 39 | func sysGet(syscallNum uintptr, path string, name string, data []byte) (int, error) { 40 | ptr, nbytes := bytePtrFromSlice(data) 41 | /* 42 | ssize_t extattr_get_file( 43 | const char *path, 44 | int attrnamespace, 45 | const char *attrname, 46 | void *data, 47 | size_t nbytes); 48 | 49 | ssize_t extattr_get_link( 50 | const char *path, 51 | int attrnamespace, 52 | const char *attrname, 53 | void *data, 54 | size_t nbytes); 55 | */ 56 | r0, _, err := syscall.Syscall6(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), 57 | EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(syscall.StringBytePtr(name))), 58 | uintptr(unsafe.Pointer(ptr)), uintptr(nbytes), 0) 59 | if err != syscall.Errno(0) { 60 | return int(r0), err 61 | } 62 | return int(r0), nil 63 | } 64 | 65 | func setxattr(path string, name string, data []byte, flags int) error { 66 | return sysSet(syscall.SYS_EXTATTR_SET_FILE, path, name, data) 67 | } 68 | 69 | func lsetxattr(path string, name string, data []byte, flags int) error { 70 | return sysSet(syscall.SYS_EXTATTR_SET_LINK, path, name, data) 71 | } 72 | 73 | func fsetxattr(f *os.File, name string, data []byte, flags int) error { 74 | return setxattr(f.Name(), name, data, flags) 75 | } 76 | 77 | // sysSet is called by setxattr and lsetxattr with the appropriate syscall 78 | // number. This works because syscalls have the same signature and return 79 | // values. 80 | func sysSet(syscallNum uintptr, path string, name string, data []byte) error { 81 | ptr, nbytes := bytePtrFromSlice(data) 82 | /* 83 | ssize_t extattr_set_file( 84 | const char *path, 85 | int attrnamespace, 86 | const char *attrname, 87 | const void *data, 88 | size_t nbytes 89 | ); 90 | 91 | ssize_t extattr_set_link( 92 | const char *path, 93 | int attrnamespace, 94 | const char *attrname, 95 | const void *data, 96 | size_t nbytes 97 | ); 98 | */ 99 | r0, _, err := syscall.Syscall6(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), 100 | EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(syscall.StringBytePtr(name))), 101 | uintptr(unsafe.Pointer(ptr)), uintptr(nbytes), 0) 102 | if err != syscall.Errno(0) { 103 | return err 104 | } 105 | if int(r0) != nbytes { 106 | return syscall.E2BIG 107 | } 108 | return nil 109 | } 110 | 111 | func removexattr(path string, name string) error { 112 | return sysRemove(syscall.SYS_EXTATTR_DELETE_FILE, path, name) 113 | } 114 | 115 | func lremovexattr(path string, name string) error { 116 | return sysRemove(syscall.SYS_EXTATTR_DELETE_LINK, path, name) 117 | } 118 | 119 | func fremovexattr(f *os.File, name string) error { 120 | return removexattr(f.Name(), name) 121 | } 122 | 123 | // sysSet is called by removexattr and lremovexattr with the appropriate syscall 124 | // number. This works because syscalls have the same signature and return 125 | // values. 126 | func sysRemove(syscallNum uintptr, path string, name string) error { 127 | /* 128 | int extattr_delete_file( 129 | const char *path, 130 | int attrnamespace, 131 | const char *attrname 132 | ); 133 | 134 | int extattr_delete_link( 135 | const char *path, 136 | int attrnamespace, 137 | const char *attrname 138 | ); 139 | */ 140 | _, _, err := syscall.Syscall(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), 141 | EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(syscall.StringBytePtr(name))), 142 | ) 143 | if err != syscall.Errno(0) { 144 | return err 145 | } 146 | return nil 147 | } 148 | 149 | func listxattr(path string, data []byte) (int, error) { 150 | return sysList(syscall.SYS_EXTATTR_LIST_FILE, path, data) 151 | } 152 | 153 | func llistxattr(path string, data []byte) (int, error) { 154 | return sysList(syscall.SYS_EXTATTR_LIST_LINK, path, data) 155 | } 156 | 157 | func flistxattr(f *os.File, data []byte) (int, error) { 158 | return listxattr(f.Name(), data) 159 | } 160 | 161 | // sysSet is called by listxattr and llistxattr with the appropriate syscall 162 | // number. This works because syscalls have the same signature and return 163 | // values. 164 | func sysList(syscallNum uintptr, path string, data []byte) (int, error) { 165 | ptr, nbytes := bytePtrFromSlice(data) 166 | /* 167 | ssize_t extattr_list_file( 168 | const char *path, 169 | int attrnamespace, 170 | void *data, 171 | size_t nbytes 172 | ); 173 | 174 | ssize_t extattr_list_link( 175 | const char *path, 176 | int attrnamespace, 177 | void *data, 178 | size_t nbytes 179 | ); 180 | */ 181 | r0, _, err := syscall.Syscall6(syscallNum, uintptr(unsafe.Pointer(syscall.StringBytePtr(path))), 182 | EXTATTR_NAMESPACE_USER, uintptr(unsafe.Pointer(ptr)), uintptr(nbytes), 0, 0) 183 | if err != syscall.Errno(0) { 184 | return int(r0), err 185 | } 186 | return int(r0), nil 187 | } 188 | 189 | // stringsFromByteSlice converts a sequence of attributes to a []string. 190 | // On FreeBSD, each entry consists of a single byte containing the length 191 | // of the attribute name, followed by the attribute name. 192 | // The name is _not_ terminated by NULL. 193 | func stringsFromByteSlice(buf []byte) (result []string) { 194 | index := 0 195 | for index < len(buf) { 196 | next := index + 1 + int(buf[index]) 197 | result = append(result, string(buf[index+1:next])) 198 | index = next 199 | } 200 | return 201 | } 202 | -------------------------------------------------------------------------------- /xattr_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package xattr 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | // See https://opensource.apple.com/source/xnu/xnu-1504.15.3/bsd/sys/xattr.h.auto.html 14 | const ( 15 | // XATTR_SUPPORTED will be true if the current platform is supported 16 | XATTR_SUPPORTED = true 17 | 18 | XATTR_NOFOLLOW = 0x0001 19 | XATTR_CREATE = 0x0002 20 | XATTR_REPLACE = 0x0004 21 | XATTR_NOSECURITY = 0x0008 22 | XATTR_NODEFAULT = 0x0010 23 | XATTR_SHOWCOMPRESSION = 0x0020 24 | 25 | // ENOATTR is not exported by the syscall package on Linux, because it is 26 | // an alias for ENODATA. We export it here so it is available on all 27 | // our supported platforms. 28 | ENOATTR = syscall.ENOATTR 29 | ) 30 | 31 | func getxattr(path string, name string, data []byte) (int, error) { 32 | return unix.Getxattr(path, name, data) 33 | } 34 | 35 | func lgetxattr(path string, name string, data []byte) (int, error) { 36 | return unix.Lgetxattr(path, name, data) 37 | } 38 | 39 | func fgetxattr(f *os.File, name string, data []byte) (int, error) { 40 | return getxattr(f.Name(), name, data) 41 | } 42 | 43 | func setxattr(path string, name string, data []byte, flags int) error { 44 | return unix.Setxattr(path, name, data, flags) 45 | } 46 | 47 | func lsetxattr(path string, name string, data []byte, flags int) error { 48 | return unix.Lsetxattr(path, name, data, flags) 49 | } 50 | 51 | func fsetxattr(f *os.File, name string, data []byte, flags int) error { 52 | return setxattr(f.Name(), name, data, flags) 53 | } 54 | 55 | func removexattr(path string, name string) error { 56 | return unix.Removexattr(path, name) 57 | } 58 | 59 | func lremovexattr(path string, name string) error { 60 | return unix.Lremovexattr(path, name) 61 | } 62 | 63 | func fremovexattr(f *os.File, name string) error { 64 | return removexattr(f.Name(), name) 65 | } 66 | 67 | func listxattr(path string, data []byte) (int, error) { 68 | return unix.Listxattr(path, data) 69 | } 70 | 71 | func llistxattr(path string, data []byte) (int, error) { 72 | return unix.Llistxattr(path, data) 73 | } 74 | 75 | func flistxattr(f *os.File, data []byte) (int, error) { 76 | return listxattr(f.Name(), data) 77 | } 78 | 79 | // stringsFromByteSlice converts a sequence of attributes to a []string. 80 | // On Darwin and Linux, each entry is a NULL-terminated string. 81 | func stringsFromByteSlice(buf []byte) (result []string) { 82 | offset := 0 83 | for index, b := range buf { 84 | if b == 0 { 85 | result = append(result, string(buf[offset:index])) 86 | offset = index + 1 87 | } 88 | } 89 | return 90 | } 91 | -------------------------------------------------------------------------------- /xattr_flags_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || solaris 2 | // +build linux darwin solaris 3 | 4 | package xattr 5 | 6 | import ( 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestFlags(t *testing.T) { 13 | tmp, err := ioutil.TempFile("", "") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | defer os.Remove(tmp.Name()) 18 | 19 | err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), 0) 20 | checkIfError(t, err) 21 | 22 | err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), XATTR_CREATE) 23 | if err == nil { 24 | t.Fatalf("XATTR_CREATE should have failed because the xattr already exists") 25 | } 26 | t.Log(err) 27 | 28 | err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), XATTR_REPLACE) 29 | checkIfError(t, err) 30 | 31 | err = Remove(tmp.Name(), UserPrefix+"flags-test") 32 | checkIfError(t, err) 33 | 34 | err = SetWithFlags(tmp.Name(), UserPrefix+"flags-test", []byte("flags-test-attr-value"), XATTR_REPLACE) 35 | if err == nil { 36 | t.Fatalf("XATTR_REPLACE should have failed because there is nothing to replace") 37 | } 38 | t.Log(err) 39 | } 40 | -------------------------------------------------------------------------------- /xattr_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package xattr 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | const ( 14 | // XATTR_SUPPORTED will be true if the current platform is supported 15 | XATTR_SUPPORTED = true 16 | 17 | XATTR_CREATE = unix.XATTR_CREATE 18 | XATTR_REPLACE = unix.XATTR_REPLACE 19 | 20 | // ENOATTR is not exported by the syscall package on Linux, because it is 21 | // an alias for ENODATA. We export it here so it is available on all 22 | // our supported platforms. 23 | ENOATTR = syscall.ENODATA 24 | ) 25 | 26 | // On Linux, FUSE and CIFS filesystems can return EINTR for interrupted system 27 | // calls. This function works around this by retrying system calls until they 28 | // stop returning EINTR. 29 | // 30 | // See https://github.com/golang/go/commit/6b420169d798c7ebe733487b56ea5c3fa4aab5ce. 31 | func ignoringEINTR(fn func() error) (err error) { 32 | for { 33 | err = fn() 34 | if err != unix.EINTR { 35 | break 36 | } 37 | } 38 | return err 39 | } 40 | 41 | func getxattr(path string, name string, data []byte) (int, error) { 42 | var r int 43 | err := ignoringEINTR(func() (err error) { 44 | r, err = unix.Getxattr(path, name, data) 45 | return err 46 | }) 47 | return r, err 48 | } 49 | 50 | func lgetxattr(path string, name string, data []byte) (int, error) { 51 | var r int 52 | err := ignoringEINTR(func() (err error) { 53 | r, err = unix.Lgetxattr(path, name, data) 54 | return err 55 | }) 56 | return r, err 57 | } 58 | 59 | func fgetxattr(f *os.File, name string, data []byte) (int, error) { 60 | var r int 61 | err := ignoringEINTR(func() (err error) { 62 | r, err = unix.Fgetxattr(int(f.Fd()), name, data) 63 | return err 64 | }) 65 | return r, err 66 | } 67 | 68 | func setxattr(path string, name string, data []byte, flags int) error { 69 | return ignoringEINTR(func() (err error) { 70 | return unix.Setxattr(path, name, data, flags) 71 | }) 72 | } 73 | 74 | func lsetxattr(path string, name string, data []byte, flags int) error { 75 | return ignoringEINTR(func() (err error) { 76 | return unix.Lsetxattr(path, name, data, flags) 77 | }) 78 | } 79 | 80 | func fsetxattr(f *os.File, name string, data []byte, flags int) error { 81 | return ignoringEINTR(func() (err error) { 82 | return unix.Fsetxattr(int(f.Fd()), name, data, flags) 83 | }) 84 | } 85 | 86 | func removexattr(path string, name string) error { 87 | return ignoringEINTR(func() (err error) { 88 | return unix.Removexattr(path, name) 89 | }) 90 | } 91 | 92 | func lremovexattr(path string, name string) error { 93 | return ignoringEINTR(func() (err error) { 94 | return unix.Lremovexattr(path, name) 95 | }) 96 | } 97 | 98 | func fremovexattr(f *os.File, name string) error { 99 | return ignoringEINTR(func() (err error) { 100 | return unix.Fremovexattr(int(f.Fd()), name) 101 | }) 102 | } 103 | 104 | func listxattr(path string, data []byte) (int, error) { 105 | var r int 106 | err := ignoringEINTR(func() (err error) { 107 | r, err = unix.Listxattr(path, data) 108 | return err 109 | }) 110 | return r, err 111 | } 112 | 113 | func llistxattr(path string, data []byte) (int, error) { 114 | var r int 115 | err := ignoringEINTR(func() (err error) { 116 | r, err = unix.Llistxattr(path, data) 117 | return err 118 | }) 119 | return r, err 120 | } 121 | 122 | func flistxattr(f *os.File, data []byte) (int, error) { 123 | var r int 124 | err := ignoringEINTR(func() (err error) { 125 | r, err = unix.Flistxattr(int(f.Fd()), data) 126 | return err 127 | }) 128 | return r, err 129 | } 130 | 131 | // stringsFromByteSlice converts a sequence of attributes to a []string. 132 | // On Darwin and Linux, each entry is a NULL-terminated string. 133 | func stringsFromByteSlice(buf []byte) (result []string) { 134 | offset := 0 135 | for index, b := range buf { 136 | if b == 0 { 137 | result = append(result, string(buf[offset:index])) 138 | offset = index + 1 139 | } 140 | } 141 | return 142 | } 143 | -------------------------------------------------------------------------------- /xattr_linux_test.go: -------------------------------------------------------------------------------- 1 | package xattr 2 | 3 | import ( 4 | "syscall" 5 | "testing" 6 | ) 7 | 8 | func TestIgnoringEINTR(t *testing.T) { 9 | eintrs := 100 10 | err := ignoringEINTR(func() error { 11 | if eintrs == 0 { 12 | return nil 13 | } 14 | eintrs-- 15 | return syscall.EINTR 16 | }) 17 | 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /xattr_solaris.go: -------------------------------------------------------------------------------- 1 | //go:build solaris 2 | // +build solaris 3 | 4 | package xattr 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | const ( 14 | // XATTR_SUPPORTED will be true if the current platform is supported 15 | XATTR_SUPPORTED = true 16 | 17 | XATTR_CREATE = 0x1 18 | XATTR_REPLACE = 0x2 19 | 20 | // ENOATTR is not exported by the syscall package on Linux, because it is 21 | // an alias for ENODATA. We export it here so it is available on all 22 | // our supported platforms. 23 | ENOATTR = syscall.ENODATA 24 | ) 25 | 26 | func getxattr(path string, name string, data []byte) (int, error) { 27 | f, err := openNonblock(path) 28 | if err != nil { 29 | return 0, err 30 | } 31 | defer func() { 32 | _ = f.Close() 33 | }() 34 | return fgetxattr(f, name, data) 35 | } 36 | 37 | func lgetxattr(path string, name string, data []byte) (int, error) { 38 | return 0, unix.ENOTSUP 39 | } 40 | 41 | func fgetxattr(f *os.File, name string, data []byte) (int, error) { 42 | fd, err := unix.Openat(int(f.Fd()), name, unix.O_RDONLY|unix.O_XATTR, 0) 43 | if err != nil { 44 | return 0, err 45 | } 46 | defer func() { 47 | _ = unix.Close(fd) 48 | }() 49 | return unix.Read(fd, data) 50 | } 51 | 52 | func setxattr(path string, name string, data []byte, flags int) error { 53 | f, err := openNonblock(path) 54 | if err != nil { 55 | return err 56 | } 57 | err = fsetxattr(f, name, data, flags) 58 | if err != nil { 59 | _ = f.Close() 60 | return err 61 | } 62 | return f.Close() 63 | } 64 | 65 | func lsetxattr(path string, name string, data []byte, flags int) error { 66 | return unix.ENOTSUP 67 | } 68 | 69 | func fsetxattr(f *os.File, name string, data []byte, flags int) error { 70 | mode := unix.O_WRONLY | unix.O_XATTR 71 | if flags&XATTR_REPLACE != 0 { 72 | mode |= unix.O_TRUNC 73 | } else if flags&XATTR_CREATE != 0 { 74 | mode |= unix.O_CREAT | unix.O_EXCL 75 | } else { 76 | mode |= unix.O_CREAT | unix.O_TRUNC 77 | } 78 | fd, err := unix.Openat(int(f.Fd()), name, mode, 0666) 79 | if err != nil { 80 | return err 81 | } 82 | if _, err = unix.Write(fd, data); err != nil { 83 | _ = unix.Close(fd) 84 | return err 85 | } 86 | return unix.Close(fd) 87 | } 88 | 89 | func removexattr(path string, name string) error { 90 | mode := unix.O_RDONLY | unix.O_XATTR | unix.O_NONBLOCK | unix.O_CLOEXEC 91 | fd, err := unix.Open(path, mode, 0) 92 | if err != nil { 93 | return err 94 | } 95 | f := os.NewFile(uintptr(fd), path) 96 | defer func() { 97 | _ = f.Close() 98 | }() 99 | return fremovexattr(f, name) 100 | } 101 | 102 | func lremovexattr(path string, name string) error { 103 | return unix.ENOTSUP 104 | } 105 | 106 | func fremovexattr(f *os.File, name string) error { 107 | fd, err := unix.Openat(int(f.Fd()), ".", unix.O_XATTR, 0) 108 | if err != nil { 109 | return err 110 | } 111 | defer func() { 112 | _ = unix.Close(fd) 113 | }() 114 | return unix.Unlinkat(fd, name, 0) 115 | } 116 | 117 | func listxattr(path string, data []byte) (int, error) { 118 | f, err := openNonblock(path) 119 | if err != nil { 120 | return 0, err 121 | } 122 | defer func() { 123 | _ = f.Close() 124 | }() 125 | return flistxattr(f, data) 126 | } 127 | 128 | func llistxattr(path string, data []byte) (int, error) { 129 | return 0, unix.ENOTSUP 130 | } 131 | 132 | func flistxattr(f *os.File, data []byte) (int, error) { 133 | fd, err := unix.Openat(int(f.Fd()), ".", unix.O_RDONLY|unix.O_XATTR, 0) 134 | if err != nil { 135 | return 0, unix.ENOTSUP 136 | } 137 | xf := os.NewFile(uintptr(fd), f.Name()) 138 | defer func() { 139 | _ = xf.Close() 140 | }() 141 | names, err := xf.Readdirnames(-1) 142 | if err != nil { 143 | return 0, err 144 | } 145 | var buf []byte 146 | for _, name := range names { 147 | buf = append(buf, append([]byte(name), '\000')...) 148 | } 149 | if data == nil { 150 | return len(buf), nil 151 | } 152 | return copy(data, buf), nil 153 | } 154 | 155 | // Like os.Open, but passes O_NONBLOCK to the open(2) syscall. 156 | func openNonblock(path string) (*os.File, error) { 157 | fd, err := unix.Open(path, unix.O_RDONLY|unix.O_CLOEXEC|unix.O_NONBLOCK, 0) 158 | if err != nil { 159 | return nil, err 160 | } 161 | return os.NewFile(uintptr(fd), path), err 162 | } 163 | 164 | // stringsFromByteSlice converts a sequence of attributes to a []string. 165 | // We simulate Linux/Darwin, where each entry is a NULL-terminated string. 166 | func stringsFromByteSlice(buf []byte) (result []string) { 167 | offset := 0 168 | for index, b := range buf { 169 | if b == 0 { 170 | result = append(result, string(buf[offset:index])) 171 | offset = index + 1 172 | } 173 | } 174 | return 175 | } 176 | -------------------------------------------------------------------------------- /xattr_test.go: -------------------------------------------------------------------------------- 1 | //go:build linux || darwin || freebsd || netbsd || solaris 2 | // +build linux darwin freebsd netbsd solaris 3 | 4 | package xattr 5 | 6 | import ( 7 | "bytes" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "runtime" 13 | "syscall" 14 | "testing" 15 | 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | const UserPrefix = "user." 20 | 21 | type funcFamily struct { 22 | familyName string 23 | get func(path, name string) ([]byte, error) 24 | set func(path, name string, data []byte) error 25 | setWithFlags func(path, name string, data []byte, flags int) error 26 | remove func(path, name string) error 27 | list func(path string) ([]string, error) 28 | } 29 | 30 | // Test Get, Set, List, Remove on a regular file 31 | func TestRegularFile(t *testing.T) { 32 | families := []funcFamily{ 33 | { 34 | familyName: "Get and friends", 35 | get: Get, 36 | set: Set, 37 | setWithFlags: SetWithFlags, 38 | remove: Remove, 39 | list: List, 40 | }, 41 | { 42 | familyName: "LGet and friends", 43 | get: LGet, 44 | set: LSet, 45 | setWithFlags: LSetWithFlags, 46 | remove: LRemove, 47 | list: LList, 48 | }, 49 | { 50 | familyName: "FGet and friends", 51 | get: wrapFGet, 52 | set: wrapFSet, 53 | setWithFlags: wrapFSetWithFlags, 54 | remove: wrapFRemove, 55 | list: wrapFList, 56 | }, 57 | } 58 | for _, ff := range families { 59 | t.Run(ff.familyName, func(t *testing.T) { 60 | t.Logf("Testing %q on a regular file", ff.familyName) 61 | testRegularFile(t, ff) 62 | }) 63 | } 64 | } 65 | 66 | // testRegularFile is called with the "Get and friends" and the 67 | // "LGet and friends" function family. Both families should behave 68 | // the same on a regular file. 69 | func testRegularFile(t *testing.T, ff funcFamily) { 70 | tmp, err := ioutil.TempFile("", "") 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | defer os.Remove(tmp.Name()) 75 | 76 | xName := UserPrefix + "test" 77 | xVal := []byte("test-attr-value") 78 | 79 | // Test that SetWithFlags succeeds and that the xattr shows up in List() 80 | err = ff.setWithFlags(tmp.Name(), xName, xVal, 0) 81 | checkIfError(t, err) 82 | list, err := ff.list(tmp.Name()) 83 | checkIfError(t, err) 84 | found := false 85 | for _, name := range list { 86 | if name == xName { 87 | found = true 88 | } 89 | } 90 | if !found { 91 | t.Fatalf("List/LList did not return test attribute: %q", list) 92 | } 93 | err = ff.remove(tmp.Name(), xName) 94 | checkIfError(t, err) 95 | 96 | // Test that Set succeeds and that the the xattr shows up in List() 97 | err = ff.set(tmp.Name(), xName, xVal) 98 | checkIfError(t, err) 99 | list, err = ff.list(tmp.Name()) 100 | checkIfError(t, err) 101 | found = false 102 | for _, name := range list { 103 | if name == xName { 104 | found = true 105 | } 106 | } 107 | if !found { 108 | t.Fatalf("List/LList did not return test attribute: %q", list) 109 | } 110 | 111 | var data []byte 112 | data, err = ff.get(tmp.Name(), xName) 113 | checkIfError(t, err) 114 | 115 | value := string(data) 116 | t.Log(value) 117 | if string(xVal) != value { 118 | t.Fail() 119 | } 120 | 121 | err = ff.remove(tmp.Name(), xName) 122 | checkIfError(t, err) 123 | } 124 | 125 | // Test that setting an xattr with an empty value works. 126 | func TestNoData(t *testing.T) { 127 | tmp, err := ioutil.TempFile("", "") 128 | if err != nil { 129 | t.Fatal(err) 130 | } 131 | defer os.Remove(tmp.Name()) 132 | 133 | err = Set(tmp.Name(), UserPrefix+"test", []byte{}) 134 | checkIfError(t, err) 135 | 136 | list, err := List(tmp.Name()) 137 | checkIfError(t, err) 138 | 139 | found := false 140 | for _, name := range list { 141 | if name == UserPrefix+"test" { 142 | found = true 143 | } 144 | } 145 | 146 | if !found { 147 | t.Fatal("Listxattr did not return test attribute") 148 | } 149 | } 150 | 151 | // Test that Get/LGet, Set/LSet etc operate as expected on symlinks. The 152 | // functions should behave differently when operating on a symlink. 153 | func TestSymlink(t *testing.T) { 154 | if runtime.GOOS == "solaris" || runtime.GOOS == "illumos" { 155 | t.Skipf("extended attributes aren't supported for symlinks on %s", runtime.GOOS) 156 | } 157 | dir, err := ioutil.TempDir("", "") 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | s := dir + "/symlink1" 162 | err = os.Symlink(dir+"/some/nonexistent/path", s) 163 | if err != nil { 164 | t.Fatal(err) 165 | } 166 | xName := UserPrefix + "TestSymlink" 167 | xVal := []byte("test") 168 | 169 | // Test Set/LSet 170 | if err := Set(s, xName, xVal); err == nil { 171 | t.Error("Set on a broken symlink should fail, but did not") 172 | } 173 | err = LSet(s, xName, xVal) 174 | errno := unpackSysErr(err) 175 | setOk := true 176 | if runtime.GOOS == "linux" && errno == syscall.EPERM { 177 | // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/xattr.c?h=v4.17-rc5#n122 : 178 | // In the user.* namespace, only regular files and directories can have 179 | // extended attributes. 180 | t.Log("got EPERM, adjusting test scope") 181 | setOk = false 182 | } else { 183 | checkIfError(t, err) 184 | } 185 | 186 | // Test List/LList 187 | _, err = List(s) 188 | errno = unpackSysErr(err) 189 | if errno != syscall.ENOENT { 190 | t.Errorf("List() on a broken symlink should fail with ENOENT, got %q", errno) 191 | } 192 | data, err := LList(s) 193 | checkIfError(t, err) 194 | if setOk { 195 | found := false 196 | for _, n := range data { 197 | if n == xName { 198 | found = true 199 | break 200 | } 201 | } 202 | if !found { 203 | t.Errorf("xattr %q did not show up in Llist output: %q", xName, data) 204 | } 205 | } 206 | 207 | // Test Get/LGet 208 | _, err = Get(s, xName) 209 | errno = unpackSysErr(err) 210 | if errno != syscall.ENOENT { 211 | t.Errorf("Get() on a broken symlink should fail with ENOENT, got %q", errno) 212 | } 213 | val, err := LGet(s, xName) 214 | if setOk { 215 | checkIfError(t, err) 216 | if !bytes.Equal(xVal, val) { 217 | t.Errorf("wrong xattr value: want=%q have=%q", xVal, val) 218 | } 219 | } else { 220 | errno = unpackSysErr(err) 221 | if errno != ENOATTR { 222 | t.Errorf("expected ENOATTR, got %q", errno) 223 | } 224 | } 225 | 226 | // Test Remove/Lremove 227 | err = Remove(s, xName) 228 | errno = unpackSysErr(err) 229 | if errno != syscall.ENOENT { 230 | t.Errorf("Remove() on a broken symlink should fail with ENOENT, got %q", errno) 231 | } 232 | err = LRemove(s, xName) 233 | if setOk { 234 | checkIfError(t, err) 235 | } else { 236 | errno = unpackSysErr(err) 237 | if errno != syscall.EPERM { 238 | t.Errorf("expected EPERM, got %q", errno) 239 | } 240 | } 241 | } 242 | 243 | // Verify that Get() handles values larger than the default buffer size (1 KB) 244 | func TestLargeVal(t *testing.T) { 245 | tmp, err := ioutil.TempFile("", "") 246 | if err != nil { 247 | t.Fatal(err) 248 | } 249 | defer os.Remove(tmp.Name()) 250 | path := tmp.Name() 251 | 252 | key := UserPrefix + "TestERANGE" 253 | // On ext4, key + value length must be <= 4096. Use 4000 so we can test 254 | // reliably on ext4. 255 | val := bytes.Repeat([]byte("z"), 4000) 256 | err = Set(path, key, val) 257 | checkIfError(t, err) 258 | 259 | val2, err := Get(path, key) 260 | checkIfError(t, err) 261 | if !bytes.Equal(val, val2) { 262 | t.Errorf("wrong result from Get: want=%s have=%s", string(val), string(val2)) 263 | } 264 | } 265 | 266 | // Get should work on a FIFO that is not opened by any other process. 267 | // This is mainly relevant on Solaris, where getting the attributes 268 | // requires opening the file and doing that the wrong way blocks the caller. 269 | func TestFIFO(t *testing.T) { 270 | d, err := ioutil.TempDir("", "pkg-xattr-testfifo-*") 271 | if err != nil { 272 | t.Fatal(err) 273 | } 274 | t.Log("tempdir:", d) 275 | defer os.RemoveAll(d) 276 | 277 | fifo := filepath.Join(d, "fifo") 278 | err = unix.Mkfifo(fifo, 0777) 279 | if err != nil { 280 | t.Fatal(err) 281 | } 282 | 283 | // We only care about this not blocking. 284 | _ = Set(fifo, "foo", []byte("bar")) 285 | _, _ = Get(fifo, "foo") 286 | _, _ = List(fifo) 287 | _ = Remove(fifo, "foo") 288 | } 289 | 290 | // checkIfError calls t.Skip() if the underlying syscall.Errno is 291 | // ENOTSUP or EOPNOTSUPP. It calls t.Fatal() on any other non-zero error. 292 | func checkIfError(t *testing.T, err error) { 293 | errno := unpackSysErr(err) 294 | if errno == syscall.Errno(0) { 295 | return 296 | } 297 | // check if filesystem supports extended attributes 298 | if errno == syscall.Errno(syscall.ENOTSUP) || errno == syscall.Errno(syscall.EOPNOTSUPP) { 299 | t.Skip("Skipping test - filesystem does not support extended attributes") 300 | } else { 301 | t.Fatal(err) 302 | } 303 | } 304 | 305 | // unpackSysErr unpacks the underlying syscall.Errno from an error value 306 | // returned by Get/Set/... 307 | func unpackSysErr(err error) syscall.Errno { 308 | if err == nil { 309 | return syscall.Errno(0) 310 | } 311 | err2, ok := err.(*Error) 312 | if !ok { 313 | log.Panicf("cannot unpack err=%#v", err) 314 | } 315 | err3, ok := err2.Err.(syscall.Errno) 316 | if !ok { 317 | log.Panicf("cannot unpack err2=%#v", err2) 318 | } 319 | return err3 320 | } 321 | 322 | // wrappers to adapt "F" variants to the test 323 | 324 | func wrapFGet(path, name string) ([]byte, error) { 325 | f, err := os.Open(path) 326 | if err != nil { 327 | return nil, err 328 | } 329 | defer f.Close() 330 | return FGet(f, name) 331 | } 332 | 333 | func wrapFSet(path, name string, data []byte) error { 334 | f, err := os.Open(path) 335 | if err != nil { 336 | return err 337 | } 338 | defer f.Close() 339 | return FSet(f, name, data) 340 | } 341 | 342 | func wrapFSetWithFlags(path, name string, data []byte, flags int) error { 343 | f, err := os.Open(path) 344 | if err != nil { 345 | return err 346 | } 347 | defer f.Close() 348 | return FSetWithFlags(f, name, data, flags) 349 | } 350 | 351 | func wrapFRemove(path, name string) error { 352 | f, err := os.Open(path) 353 | if err != nil { 354 | return err 355 | } 356 | defer f.Close() 357 | return FRemove(f, name) 358 | } 359 | 360 | func wrapFList(path string) ([]string, error) { 361 | f, err := os.Open(path) 362 | if err != nil { 363 | return nil, err 364 | } 365 | defer f.Close() 366 | return FList(f) 367 | } 368 | -------------------------------------------------------------------------------- /xattr_unsupported.go: -------------------------------------------------------------------------------- 1 | //go:build !linux && !freebsd && !netbsd && !darwin && !solaris 2 | // +build !linux,!freebsd,!netbsd,!darwin,!solaris 3 | 4 | package xattr 5 | 6 | import ( 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | const ( 12 | // We need to use the default for non supported operating systems 13 | ENOATTR = syscall.Errno(0x59) 14 | ) 15 | 16 | // XATTR_SUPPORTED will be true if the current platform is supported 17 | const XATTR_SUPPORTED = false 18 | 19 | func getxattr(path string, name string, data []byte) (int, error) { 20 | return 0, nil 21 | } 22 | 23 | func lgetxattr(path string, name string, data []byte) (int, error) { 24 | return 0, nil 25 | } 26 | 27 | func fgetxattr(f *os.File, name string, data []byte) (int, error) { 28 | return 0, nil 29 | } 30 | 31 | func setxattr(path string, name string, data []byte, flags int) error { 32 | return nil 33 | } 34 | 35 | func lsetxattr(path string, name string, data []byte, flags int) error { 36 | return nil 37 | } 38 | 39 | func fsetxattr(f *os.File, name string, data []byte, flags int) error { 40 | return nil 41 | } 42 | 43 | func removexattr(path string, name string) error { 44 | return nil 45 | } 46 | 47 | func lremovexattr(path string, name string) error { 48 | return nil 49 | } 50 | 51 | func fremovexattr(f *os.File, name string) error { 52 | return nil 53 | } 54 | 55 | func listxattr(path string, data []byte) (int, error) { 56 | return 0, nil 57 | } 58 | 59 | func llistxattr(path string, data []byte) (int, error) { 60 | return 0, nil 61 | } 62 | 63 | func flistxattr(f *os.File, data []byte) (int, error) { 64 | return 0, nil 65 | } 66 | 67 | // dummy 68 | func stringsFromByteSlice(buf []byte) (result []string) { 69 | return []string{} 70 | } 71 | --------------------------------------------------------------------------------