mount: forward extended attributes on windows (#10554)

weed/mount implements all four xattr operations and the filer stores the
values, but the Windows adapter overrode none of them, so cgofuse's
defaults answered every call with 'not implemented'. WinFsp advertises
extended attribute support either way, because cgofuse registers the
callbacks unconditionally, so applications were told the volume has them
and then refused on every use. Attributes written from Linux were
invisible from Windows.

Untested in CI: exercising Windows extended attributes needs the native
NtSetEaFile path rather than anything in os or PowerShell.
This commit is contained in:
Chris Lu
2026-08-03 21:55:59 -07:00
committed by GitHub
parent e377149d39
commit a0e278f86f
3 changed files with 115 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
package mount
import "testing"
// The Windows adapter forwards cgofuse's xattr flags to SetXAttr untouched,
// which only holds while both sides number them the same. cgofuse's side is
// pinned in weed/mount/winfsp; this pins ours, so editing either alone fails
// rather than silently turning a create into a replace.
func TestXattrFlagValues(t *testing.T) {
if xattr_CREATE != 1 {
t.Errorf("xattr_CREATE = %d, want 1", xattr_CREATE)
}
if xattr_REPLACE != 2 {
t.Errorf("xattr_REPLACE = %d, want 2", xattr_REPLACE)
}
}
+9
View File
@@ -76,3 +76,12 @@ func TestOpenFlagTranslation(t *testing.T) {
t.Errorf("access mode not preserved: %#x", got)
}
}
// Setxattr passes cgofuse's flags straight to the raw filesystem, which is
// only correct while the two number them identically. The other side is
// pinned by TestXattrFlagValues in weed/mount, so editing either alone fails.
func TestXattrFlagValues(t *testing.T) {
if cgofuse.XATTR_CREATE != 1 || cgofuse.XATTR_REPLACE != 2 {
t.Fatalf("cgofuse xattr flags moved: create=%d replace=%d", cgofuse.XATTR_CREATE, cgofuse.XATTR_REPLACE)
}
}
+90
View File
@@ -0,0 +1,90 @@
package winfsp
import (
"bytes"
"github.com/seaweedfs/go-fuse/v2/fuse"
)
// xattrBufferSize bounds one attribute value and one name listing. The mount
// stores extended attributes in the entry itself, so they are small by
// construction; this is the ceiling WinFsp will accept in a single reply.
const xattrBufferSize = 64 * 1024
func (w *WinFS) Getxattr(path string, name string) (int, []byte) {
inode, ref, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status), nil
}
defer ref.release()
dest := make([]byte, xattrBufferSize)
size, status := w.wfs.GetXAttr(never, ptr(w.caller(inode)), name, dest)
if status != fuse.OK {
return toErrno(status), nil
}
return 0, dest[:size]
}
func (w *WinFS) Setxattr(path string, name string, value []byte, flags int) int {
if w.denied() {
return -eROFS
}
inode, ref, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
defer ref.release()
// cgofuse and the raw filesystem number XATTR_CREATE and XATTR_REPLACE the
// same, so the flags pass straight through; TestXattrFlagValues pins that.
in := &fuse.SetXAttrIn{InHeader: w.caller(inode)}
in.Flags = uint32(flags)
in.Size = uint32(len(value))
return toErrno(w.wfs.SetXAttr(never, in, name, value))
}
func (w *WinFS) Removexattr(path string, name string) int {
if w.denied() {
return -eROFS
}
inode, ref, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
defer ref.release()
return toErrno(w.wfs.RemoveXAttr(never, ptr(w.caller(inode)), name))
}
func (w *WinFS) Listxattr(path string, fill func(name string) bool) int {
inode, ref, status := w.resolve(path)
if status != fuse.OK {
return toErrno(status)
}
defer ref.release()
dest := make([]byte, xattrBufferSize)
size, status := w.wfs.ListXAttr(never, ptr(w.caller(inode)), dest)
if status != fuse.OK {
return toErrno(status)
}
for _, name := range splitXattrNames(dest[:size]) {
if !fill(name) {
break
}
}
return 0
}
// splitXattrNames unpacks the NUL-separated listing the raw filesystem
// produces. A trailing NUL terminates the last name rather than starting an
// empty one.
func splitXattrNames(packed []byte) []string {
var names []string
for _, name := range bytes.Split(packed, []byte{0}) {
if len(name) > 0 {
names = append(names, string(name))
}
}
return names
}