| //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) |
| } |