// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT //go:build linux package nfsfs import ( "os" "strings" "syscall" ) // xattrNamespace is the only namespace this backend serves: RFC 8276 // section 3.3 names the attributes with their namespace, and the local // mapping of the Linux server is the user namespace. const xattrNamespace = "user." // Flags of the xattr system calls. const ( xattrCreateFlag = 0x1 xattrReplaceFlag = 0x2 ) // xattrPath resolves a handle for the xattr family: the registered path // must still name the handle's device, inode and kind. A symlink is // refused the way the kernel refuses the user namespace on one, so the // path calls never follow a link out of the export. func (l *Local) xattrPath(h Handle) (string, error) { _, path, fi, err := l.revalidate(h) if err != nil { return "", err } if fi.Mode()&os.ModeSymlink != 0 { return "", syscall.EPERM } return path, nil } // GetXattr reads one named attribute of the object. func (l *Local) GetXattr(h Handle, name string, max int) ([]byte, error) { path, err := l.xattrPath(h) if err != nil { return nil, err } if !strings.HasPrefix(name, xattrNamespace) { return nil, ErrNoXattr } buf := make([]byte, maxOr(max, 256)) for { n, err := syscall.Getxattr(path, name, buf) if err == syscall.ERANGE { if len(buf) > 1<<20 { return nil, syscall.ERANGE } buf = make([]byte, len(buf)*2) continue } if err == syscall.ENODATA { return nil, ErrNoXattr } if err != nil { return nil, err } return buf[:n], nil } } // SetXattr writes one named attribute under the RFC 8276 mode. func (l *Local) SetXattr(h Handle, name string, value []byte, mode uint32) error { path, err := l.xattrPath(h) if err != nil { return err } if !strings.HasPrefix(name, xattrNamespace) { return syscall.EOPNOTSUPP } flags := 0 switch mode { case XattrModeCreate: flags = xattrCreateFlag case XattrModeReplace: flags = xattrReplaceFlag } return syscall.Setxattr(path, name, value, flags) } // ListXattr names the user namespace attributes of the object. func (l *Local) ListXattr(h Handle, max int) ([]string, error) { path, err := l.xattrPath(h) if err != nil { return nil, err } buf := make([]byte, maxOr(max, 1024)) for { n, err := syscall.Listxattr(path, buf) if err == syscall.ERANGE { if len(buf) > 1<<20 { return nil, syscall.ERANGE } buf = make([]byte, len(buf)*2) continue } if err != nil { return nil, err } var names []string for entry := range strings.SplitSeq(string(buf[:n]), "\x00") { if strings.HasPrefix(entry, xattrNamespace) { names = append(names, entry) } } return names, nil } } // RemoveXattr deletes one named attribute. func (l *Local) RemoveXattr(h Handle, name string) error { path, err := l.xattrPath(h) if err != nil { return err } if !strings.HasPrefix(name, xattrNamespace) { return ErrNoXattr } if err := syscall.Removexattr(path, name); err == syscall.ENODATA { return ErrNoXattr } else if err != nil { return err } return nil } // maxOr replaces a zero budget with the given default. func maxOr(max, def int) int { if max == 0 || max > 1<<20 { return def } return max }