Repository: ceph/go-ceph
Pull request #339 (2020-07-31 15:08:56)

cephfs: add path-based xattr functions
Add functions needed to read/write/etc xattrs from paths. Both "regular" and "l-prefix" functions are present.

The spelling/case of the"l-prefix" functions are based on the naming approach in Go's os package which uses "Lstat" or "Lchown" rather than "LChown" so here we use "LsetXattr" rather than "LSetXattr" for example.

Fixes: #255 

## Checklist
- [x] Added tests for features and functional changes
- [x] Public functions and types are documented
- [x] Standard formatting is applied to Go code



Patch:
diff --git a/cephfs/file_xattr.go b/cephfs/file_xattr.go
index 5509e58..cd9306a 100644
--- a/cephfs/file_xattr.go
+++ b/cephfs/file_xattr.go
@@ -103,7 +103,7 @@ func (f *File) GetXattr(name string) ([]byte, error) {
 }
 
 // ListXattr returns a slice containing strings for the name of each xattr set
-// on the fie.
+// on the file.
 //
 // Implements:
 //  int ceph_flistxattr(struct ceph_mount_info *cmount, int fd, char *list, size_t size);
diff --git a/cephfs/path_xattr.go b/cephfs/path_xattr.go
new file mode 100644
index 0000000..2f166cd
--- /dev/null
+++ b/cephfs/path_xattr.go
@@ -0,0 +1,283 @@
+package cephfs
+
+/*
+#cgo LDFLAGS: -lcephfs
+#cgo CPPFLAGS: -D_FILE_OFFSET_BITS=64
+#define _GNU_SOURCE
+#include <stdlib.h>
+#include <cephfs/libcephfs.h>
+*/
+import "C"
+
+import (
+	"unsafe"
+
+	"github.com/ceph/go-ceph/internal/cutil"
+	"github.com/ceph/go-ceph/internal/retry"
+)
+
+// SetXattr sets an extended attribute on the file at the supplied path.
+//
+// NOTE: Attempting to set an xattr value with an empty value may cause
+// the xattr to be unset. Please refer to https://tracker.ceph.com/issues/46084
+//
+// Implements:
+//  int ceph_setxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
+//                    const void *value, size_t size, int flags);
+func (mount *MountInfo) SetXattr(path, name string, value []byte, flags XattrFlags) error {
+	if err := mount.validate(); err != nil {
+		return err
+	}
+	if name == "" {
+		return errInvalid
+	}
+	var vptr unsafe.Pointer
+	if len(value) > 0 {
+		vptr = unsafe.Pointer(&value[0])
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	cName := C.CString(name)
+	defer C.free(unsafe.Pointer(cName))
+
+	ret := C.ceph_setxattr(
+		mount.mount,
+		cPath,
+		cName,
+		vptr,
+		C.size_t(len(value)),
+		C.int(flags))
+	return getError(ret)
+}
+
+// GetXattr gets an extended attribute from the file at the supplied path.
+//
+// Implements:
+//  int ceph_getxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
+//                    void *value, size_t size);
+func (mount *MountInfo) GetXattr(path, name string) ([]byte, error) {
+	if err := mount.validate(); err != nil {
+		return nil, err
+	}
+	if name == "" {
+		return nil, errInvalid
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	cName := C.CString(name)
+	defer C.free(unsafe.Pointer(cName))
+
+	var (
+		ret C.int
+		err error
+		buf []byte
+	)
+	// range from 1k to 64KiB
+	retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
+		buf = make([]byte, size)
+		ret = C.ceph_getxattr(
+			mount.mount,
+			cPath,
+			cName,
+			unsafe.Pointer(&buf[0]),
+			C.size_t(size))
+		err = getErrorIfNegative(ret)
+		return retry.DoubleSize.If(err == errRange)
+	})
+	if err != nil {
+		return nil, err
+	}
+	return buf[:ret], nil
+}
+
+// ListXattr returns a slice containing strings for the name of each xattr set
+// on the file at the supplied path.
+//
+// Implements:
+//  int ceph_listxattr(struct ceph_mount_info *cmount, const char *path, char *list, size_t size);
+func (mount *MountInfo) ListXattr(path string) ([]string, error) {
+	if err := mount.validate(); err != nil {
+		return nil, err
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+
+	var (
+		ret C.int
+		err error
+		buf []byte
+	)
+	// range from 1k to 64KiB
+	retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
+		buf = make([]byte, size)
+		ret = C.ceph_listxattr(
+			mount.mount,
+			cPath,
+			(*C.char)(unsafe.Pointer(&buf[0])),
+			C.size_t(size))
+		err = getErrorIfNegative(ret)
+		return retry.DoubleSize.If(err == errRange)
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	names := cutil.SplitSparseBuffer(buf[:ret])
+	return names, nil
+}
+
+// RemoveXattr removes the named xattr from the open file.
+//
+// Implements:
+//  int ceph_removexattr(struct ceph_mount_info *cmount, const char *path, const char *name);
+func (mount *MountInfo) RemoveXattr(path, name string) error {
+	if err := mount.validate(); err != nil {
+		return err
+	}
+	if name == "" {
+		return errInvalid
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	cName := C.CString(name)
+	defer C.free(unsafe.Pointer(cName))
+
+	ret := C.ceph_removexattr(
+		mount.mount,
+		cPath,
+		cName)
+	return getError(ret)
+}
+
+// LsetXattr sets an extended attribute on the file at the supplied path.
+//
+// NOTE: Attempting to set an xattr value with an empty value may cause
+// the xattr to be unset. Please refer to https://tracker.ceph.com/issues/46084
+//
+// Implements:
+//  int ceph_lsetxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
+//                    const void *value, size_t size, int flags);
+func (mount *MountInfo) LsetXattr(path, name string, value []byte, flags XattrFlags) error {
+	if err := mount.validate(); err != nil {
+		return err
+	}
+	if name == "" {
+		return errInvalid
+	}
+	var vptr unsafe.Pointer
+	if len(value) > 0 {
+		vptr = unsafe.Pointer(&value[0])
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	cName := C.CString(name)
+	defer C.free(unsafe.Pointer(cName))
+
+	ret := C.ceph_lsetxattr(
+		mount.mount,
+		cPath,
+		cName,
+		vptr,
+		C.size_t(len(value)),
+		C.int(flags))
+	return getError(ret)
+}
+
+// LgetXattr gets an extended attribute from the file at the supplied path.
+//
+// Implements:
+//  int ceph_lgetxattr(struct ceph_mount_info *cmount, const char *path, const char *name,
+//                    void *value, size_t size);
+func (mount *MountInfo) LgetXattr(path, name string) ([]byte, error) {
+	if err := mount.validate(); err != nil {
+		return nil, err
+	}
+	if name == "" {
+		return nil, errInvalid
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	cName := C.CString(name)
+	defer C.free(unsafe.Pointer(cName))
+
+	var (
+		ret C.int
+		err error
+		buf []byte
+	)
+	// range from 1k to 64KiB
+	retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
+		buf = make([]byte, size)
+		ret = C.ceph_lgetxattr(
+			mount.mount,
+			cPath,
+			cName,
+			unsafe.Pointer(&buf[0]),
+			C.size_t(size))
+		err = getErrorIfNegative(ret)
+		return retry.DoubleSize.If(err == errRange)
+	})
+	if err != nil {
+		return nil, err
+	}
+	return buf[:ret], nil
+}
+
+// LlistXattr returns a slice containing strings for the name of each xattr set
+// on the file at the supplied path.
+//
+// Implements:
+//  int ceph_llistxattr(struct ceph_mount_info *cmount, const char *path, char *list, size_t size);
+func (mount *MountInfo) LlistXattr(path string) ([]string, error) {
+	if err := mount.validate(); err != nil {
+		return nil, err
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+
+	var (
+		ret C.int
+		err error
+		buf []byte
+	)
+	// range from 1k to 64KiB
+	retry.WithSizes(1024, 1<<16, func(size int) retry.Hint {
+		buf = make([]byte, size)
+		ret = C.ceph_llistxattr(
+			mount.mount,
+			cPath,
+			(*C.char)(unsafe.Pointer(&buf[0])),
+			C.size_t(size))
+		err = getErrorIfNegative(ret)
+		return retry.DoubleSize.If(err == errRange)
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	names := cutil.SplitSparseBuffer(buf[:ret])
+	return names, nil
+}
+
+// LremoveXattr removes the named xattr from the open file.
+//
+// Implements:
+//  int ceph_lremovexattr(struct ceph_mount_info *cmount, const char *path, const char *name);
+func (mount *MountInfo) LremoveXattr(path, name string) error {
+	if err := mount.validate(); err != nil {
+		return err
+	}
+	if name == "" {
+		return errInvalid
+	}
+	cPath := C.CString(path)
+	defer C.free(unsafe.Pointer(cPath))
+	cName := C.CString(name)
+	defer C.free(unsafe.Pointer(cName))
+
+	ret := C.ceph_lremovexattr(
+		mount.mount,
+		cPath,
+		cName)
+	return getError(ret)
+}


Test patch:
diff --git a/cephfs/file_xattr_test.go b/cephfs/file_xattr_test.go
index 6939604..b284a9f 100644
--- a/cephfs/file_xattr_test.go
+++ b/cephfs/file_xattr_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-var samples = []struct {
+var xattrSamples = []struct {
 	name  string
 	value []byte
 }{
@@ -47,7 +47,7 @@ func TestGetSetXattr(t *testing.T) {
 		assert.NoError(t, mount.Unlink(fname))
 	}()
 
-	for _, s := range samples {
+	for _, s := range xattrSamples {
 		t.Run("roundTrip-"+s.name, func(t *testing.T) {
 			err := f.SetXattr(s.name, s.value, XattrDefault)
 			assert.NoError(t, err)
@@ -74,9 +74,9 @@ func TestGetSetXattr(t *testing.T) {
 
 	t.Run("invalidFile", func(t *testing.T) {
 		f1 := &File{}
-		err := f1.SetXattr(samples[0].name, samples[0].value, XattrDefault)
+		err := f1.SetXattr(xattrSamples[0].name, xattrSamples[0].value, XattrDefault)
 		assert.Error(t, err)
-		_, err = f1.GetXattr(samples[0].name)
+		_, err = f1.GetXattr(xattrSamples[0].name)
 		assert.Error(t, err)
 	})
 }
@@ -94,27 +94,27 @@ func TestListXattr(t *testing.T) {
 	}()
 
 	t.Run("listXattrs1", func(t *testing.T) {
-		for _, s := range samples[:1] {
+		for _, s := range xattrSamples[:1] {
 			err := f.SetXattr(s.name, s.value, XattrDefault)
 			assert.NoError(t, err)
 		}
 		xl, err := f.ListXattr()
 		assert.NoError(t, err)
 		assert.Len(t, xl, 1)
-		assert.Contains(t, xl, samples[0].name)
+		assert.Contains(t, xl, xattrSamples[0].name)
 	})
 
 	t.Run("listXattrs2", func(t *testing.T) {
-		for _, s := range samples {
+		for _, s := range xattrSamples {
 			err := f.SetXattr(s.name, s.value, XattrDefault)
 			assert.NoError(t, err)
 		}
 		xl, err := f.ListXattr()
 		assert.NoError(t, err)
 		assert.Len(t, xl, 3)
-		assert.Contains(t, xl, samples[0].name)
-		assert.Contains(t, xl, samples[1].name)
-		assert.Contains(t, xl, samples[2].name)
+		assert.Contains(t, xl, xattrSamples[0].name)
+		assert.Contains(t, xl, xattrSamples[1].name)
+		assert.Contains(t, xl, xattrSamples[2].name)
 	})
 
 	t.Run("invalidFile", func(t *testing.T) {
@@ -137,14 +137,14 @@ func TestRemoveXattr(t *testing.T) {
 	}()
 
 	t.Run("removeXattr", func(t *testing.T) {
-		s := samples[0]
+		s := xattrSamples[0]
 		err := f.SetXattr(s.name, s.value, XattrDefault)
 		err = f.RemoveXattr(s.name)
 		assert.NoError(t, err)
 	})
 
 	t.Run("removeMissingXattr", func(t *testing.T) {
-		s := samples[1]
+		s := xattrSamples[1]
 		err := f.RemoveXattr(s.name)
 		assert.Error(t, err)
 	})
@@ -156,7 +156,7 @@ func TestRemoveXattr(t *testing.T) {
 
 	t.Run("invalidFile", func(t *testing.T) {
 		f1 := &File{}
-		err := f1.RemoveXattr(samples[0].name)
+		err := f1.RemoveXattr(xattrSamples[0].name)
 		assert.Error(t, err)
 	})
 }
diff --git a/cephfs/path_xattr_test.go b/cephfs/path_xattr_test.go
new file mode 100644
index 0000000..c6224c0
--- /dev/null
+++ b/cephfs/path_xattr_test.go
@@ -0,0 +1,298 @@
+package cephfs
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetSetXattrPath(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestGetSetXattrPath.txt"
+
+	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+	require.NoError(t, err)
+	assert.NoError(t, f1.Close())
+	defer func() {
+		assert.NoError(t, mount.Unlink(fname))
+	}()
+
+	for _, s := range xattrSamples {
+		t.Run("roundTrip-"+s.name, func(t *testing.T) {
+			err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
+			assert.NoError(t, err)
+			b, err := mount.GetXattr(fname, s.name)
+			assert.NoError(t, err)
+			assert.EqualValues(t, s.value, b)
+		})
+	}
+
+	t.Run("missingXattrOnGet", func(t *testing.T) {
+		_, err := mount.GetXattr(fname, "user.never-set")
+		assert.Error(t, err)
+	})
+
+	t.Run("emptyNameGet", func(t *testing.T) {
+		_, err := mount.GetXattr(fname, "")
+		assert.Error(t, err)
+	})
+
+	t.Run("emptyNameSet", func(t *testing.T) {
+		err := mount.SetXattr(fname, "", []byte("foo"), XattrDefault)
+		assert.Error(t, err)
+	})
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		err := m.SetXattr(fname, xattrSamples[0].name, xattrSamples[0].value, XattrDefault)
+		assert.Error(t, err)
+		_, err = m.GetXattr(fname, xattrSamples[0].name)
+		assert.Error(t, err)
+	})
+}
+
+func TestListXattrPath(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestListXattrPath.txt"
+
+	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+	require.NoError(t, err)
+	assert.NoError(t, f1.Close())
+	defer func() {
+		assert.NoError(t, mount.Unlink(fname))
+	}()
+
+	t.Run("listXattrs1", func(t *testing.T) {
+		for _, s := range xattrSamples[:1] {
+			err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
+			assert.NoError(t, err)
+		}
+		xl, err := mount.ListXattr(fname)
+		assert.NoError(t, err)
+		assert.Len(t, xl, 1)
+		assert.Contains(t, xl, xattrSamples[0].name)
+	})
+
+	t.Run("listXattrs2", func(t *testing.T) {
+		for _, s := range xattrSamples {
+			err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
+			assert.NoError(t, err)
+		}
+		xl, err := mount.ListXattr(fname)
+		assert.NoError(t, err)
+		assert.Len(t, xl, 3)
+		assert.Contains(t, xl, xattrSamples[0].name)
+		assert.Contains(t, xl, xattrSamples[1].name)
+		assert.Contains(t, xl, xattrSamples[2].name)
+	})
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		_, err := m.ListXattr(fname)
+		assert.Error(t, err)
+	})
+}
+
+func TestRemoveXattrPath(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestRemoveXattrPath.txt"
+
+	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+	require.NoError(t, err)
+	assert.NoError(t, f1.Close())
+	defer func() {
+		assert.NoError(t, mount.Unlink(fname))
+	}()
+
+	t.Run("removeXattr", func(t *testing.T) {
+		s := xattrSamples[0]
+		err := mount.SetXattr(fname, s.name, s.value, XattrDefault)
+		err = mount.RemoveXattr(fname, s.name)
+		assert.NoError(t, err)
+	})
+
+	t.Run("removeMissingXattr", func(t *testing.T) {
+		s := xattrSamples[1]
+		err := mount.RemoveXattr(fname, s.name)
+		assert.Error(t, err)
+	})
+
+	t.Run("emptyName", func(t *testing.T) {
+		err := mount.RemoveXattr(fname, "")
+		assert.Error(t, err)
+	})
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		err := m.RemoveXattr(fname, xattrSamples[0].name)
+		assert.Error(t, err)
+	})
+}
+
+func TestGetSetXattrLinkPath(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestGetSetXattrLinkPath.txt"
+	lname := "TestGetSetXattrLinkPath.lnk"
+
+	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+	require.NoError(t, err)
+	assert.NoError(t, f1.Close())
+	err = mount.Symlink(fname, lname)
+	require.NoError(t, err)
+	defer func() {
+		assert.NoError(t, mount.Unlink(fname))
+		assert.NoError(t, mount.Unlink(lname))
+	}()
+
+	for _, s := range xattrSamples {
+		t.Run("roundTrip-"+s.name, func(t *testing.T) {
+			err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
+			assert.NoError(t, err)
+			b, err := mount.LgetXattr(lname, s.name)
+			assert.NoError(t, err)
+			assert.EqualValues(t, s.value, b)
+		})
+	}
+
+	t.Run("linkVsFile", func(t *testing.T) {
+		s := xattrSamples[0]
+		err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
+		assert.NoError(t, err)
+
+		// not on the file
+		err = mount.LremoveXattr(fname, s.name)
+		assert.Error(t, err)
+		// on the link
+		err = mount.LremoveXattr(lname, s.name)
+		assert.NoError(t, err)
+	})
+
+	t.Run("missingXattrOnGet", func(t *testing.T) {
+		_, err := mount.LgetXattr(lname, "user.never-set")
+		assert.Error(t, err)
+	})
+
+	t.Run("emptyNameGet", func(t *testing.T) {
+		_, err := mount.LgetXattr(lname, "")
+		assert.Error(t, err)
+	})
+
+	t.Run("emptyNameSet", func(t *testing.T) {
+		err := mount.LsetXattr(lname, "", []byte("foo"), XattrDefault)
+		assert.Error(t, err)
+	})
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		err := m.LsetXattr(lname, xattrSamples[0].name, xattrSamples[0].value, XattrDefault)
+		assert.Error(t, err)
+		_, err = m.LgetXattr(lname, xattrSamples[0].name)
+		assert.Error(t, err)
+	})
+}
+
+func TestListXattrLinkPath(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestListXattrLinkPath.txt"
+	lname := "TestListXattrLinkPath.lnk"
+
+	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+	require.NoError(t, err)
+	assert.NoError(t, f1.Close())
+	err = mount.Symlink(fname, lname)
+	require.NoError(t, err)
+	defer func() {
+		assert.NoError(t, mount.Unlink(fname))
+		assert.NoError(t, mount.Unlink(lname))
+	}()
+
+	t.Run("listXattrs1", func(t *testing.T) {
+		for _, s := range xattrSamples[:1] {
+			err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
+			assert.NoError(t, err)
+		}
+
+		// not on the file
+		xl, err := mount.LlistXattr(fname)
+		assert.NoError(t, err)
+		assert.Len(t, xl, 0)
+		// on the link
+		xl, err = mount.LlistXattr(lname)
+		assert.NoError(t, err)
+		assert.Len(t, xl, 1)
+		assert.Contains(t, xl, xattrSamples[0].name)
+	})
+
+	t.Run("listXattrs2", func(t *testing.T) {
+		for _, s := range xattrSamples {
+			err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
+			assert.NoError(t, err)
+		}
+		xl, err := mount.LlistXattr(lname)
+		assert.NoError(t, err)
+		assert.Len(t, xl, 3)
+		assert.Contains(t, xl, xattrSamples[0].name)
+		assert.Contains(t, xl, xattrSamples[1].name)
+		assert.Contains(t, xl, xattrSamples[2].name)
+	})
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		_, err := m.LlistXattr(lname)
+		assert.Error(t, err)
+	})
+}
+
+func TestRemoveXattrLinkPath(t *testing.T) {
+	mount := fsConnect(t)
+	defer fsDisconnect(t, mount)
+	fname := "TestRemoveXattrLinkPath.txt"
+	lname := "TestRemoveXattrLinkPath.lnk"
+
+	f1, err := mount.Open(fname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+	require.NoError(t, err)
+	assert.NoError(t, f1.Close())
+	err = mount.Symlink(fname, lname)
+	require.NoError(t, err)
+	defer func() {
+		assert.NoError(t, mount.Unlink(fname))
+		assert.NoError(t, mount.Unlink(lname))
+	}()
+
+	t.Run("removeXattr", func(t *testing.T) {
+		s := xattrSamples[0]
+		err := mount.LsetXattr(lname, s.name, s.value, XattrDefault)
+		assert.NoError(t, err)
+
+		// not on the file
+		err = mount.LremoveXattr(fname, s.name)
+		assert.Error(t, err)
+		// on the link
+		err = mount.LremoveXattr(lname, s.name)
+		assert.NoError(t, err)
+	})
+
+	t.Run("removeMissingXattr", func(t *testing.T) {
+		s := xattrSamples[1]
+		err := mount.LremoveXattr(lname, s.name)
+		assert.Error(t, err)
+	})
+
+	t.Run("emptyName", func(t *testing.T) {
+		err := mount.LremoveXattr(lname, "")
+		assert.Error(t, err)
+	})
+
+	t.Run("invalidMount", func(t *testing.T) {
+		m := &MountInfo{}
+		err := m.LremoveXattr(lname, xattrSamples[0].name)
+		assert.Error(t, err)
+	})
+}