Files
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

254 lines
6.7 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
//go:build freebsd
package nfsfs
import (
"os"
"strings"
"syscall"
"unsafe"
)
// 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 FreeBSD server is the extattr user namespace.
const xattrNamespace = "user."
// extattrNamespaceUser is EXTATTR_NAMESPACE_USER of sys/sys/extattr.h,
// the numeric namespace the extattr family of system calls takes. The
// syscall package of FreeBSD exports the traps of the family but not
// this constant.
const extattrNamespaceUser = 0x1
// xattrPath resolves a handle for the extattr 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
}
attr, ok := strings.CutPrefix(name, xattrNamespace)
if !ok {
return nil, ErrNoXattr
}
buf := make([]byte, maxOr(max, 256))
for {
n, err := extattrGetFile(path, attr, buf)
if err == syscall.ERANGE {
if len(buf) > 1<<20 {
return nil, syscall.ERANGE
}
buf = make([]byte, len(buf)*2)
continue
}
if err == syscall.ENOATTR {
return nil, ErrNoXattr
}
if err != nil {
return nil, err
}
return buf[:n], nil
}
}
// SetXattr writes one named attribute under the RFC 8276 mode. The
// extattr interface of FreeBSD carries no create and replace flags, so
// the two strict modes ask for the attribute first and write it after:
// a create of an existing name answers EEXIST and a replace of a
// missing one ENOATTR, the same answers the flags of Linux produce.
func (l *Local) SetXattr(h Handle, name string, value []byte, mode uint32) error {
path, err := l.xattrPath(h)
if err != nil {
return err
}
attr, ok := strings.CutPrefix(name, xattrNamespace)
if !ok {
return syscall.EOPNOTSUPP
}
switch mode {
case XattrModeCreate, XattrModeReplace:
_, err := extattrGetFile(path, attr, nil)
switch {
case err == nil && mode == XattrModeCreate:
return syscall.EEXIST
case err == syscall.ENOATTR && mode == XattrModeReplace:
return syscall.ENOATTR
case err != nil && err != syscall.ENOATTR:
return err
}
}
return extattrSetFile(path, attr, value)
}
// ListXattr names the user namespace attributes of the object. The list
// of extattr_list_file is a sequence of one length byte and name pairs
// with no separators, extattr(2), and its names carry no namespace, so
// each name is dressed with the namespace prefix the protocol speaks.
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 := extattrListFile(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
}
buf = buf[:n]
break
}
var names []string
for len(buf) > 0 {
size := int(buf[0])
if size+1 > len(buf) {
break
}
names = append(names, xattrNamespace+string(buf[1:1+size]))
buf = buf[1+size:]
}
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
}
attr, ok := strings.CutPrefix(name, xattrNamespace)
if !ok {
return ErrNoXattr
}
if err := extattrDeleteFile(path, attr); err == syscall.ENOATTR {
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
}
// extattrGetFile reads the attribute attr of path into buf, or names its
// size when buf is nil, extattr(2).
func extattrGetFile(path, attr string, buf []byte) (int, error) {
name, err := syscall.ByteSliceFromString(attr)
if err != nil {
return 0, err
}
// SAFETY: the kernel reads the buffer for the length of the call
// only, and data stays alive through the unsafe pointer until the
// system call returns.
var data unsafe.Pointer
if len(buf) > 0 {
data = unsafe.Pointer(&buf[0])
}
n, _, errno := syscall.Syscall6(syscall.SYS_EXTATTR_GET_FILE,
uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
extattrNamespaceUser,
uintptr(unsafe.Pointer(&name[0])),
uintptr(data),
uintptr(len(buf)),
0)
if errno != 0 {
return 0, errno
}
return int(n), nil
}
// extattrSetFile writes value into the attribute attr of path,
// extattr(2).
func extattrSetFile(path, attr string, value []byte) error {
name, err := syscall.ByteSliceFromString(attr)
if err != nil {
return err
}
// SAFETY: the kernel reads the buffer for the length of the call
// only, and value stays alive through the unsafe pointer until the
// system call returns.
var data unsafe.Pointer
if len(value) > 0 {
data = unsafe.Pointer(&value[0])
}
_, _, errno := syscall.Syscall6(syscall.SYS_EXTATTR_SET_FILE,
uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
extattrNamespaceUser,
uintptr(unsafe.Pointer(&name[0])),
uintptr(data),
uintptr(len(value)),
0)
if errno != 0 {
return errno
}
return nil
}
// extattrListFile names the user namespace attributes of path into buf,
// or names the size of the list when buf is nil, extattr(2).
func extattrListFile(path string, buf []byte) (int, error) {
// SAFETY: the kernel writes the buffer for the length of the call
// only, and buf stays alive through the unsafe pointer until the
// system call returns.
var data unsafe.Pointer
if len(buf) > 0 {
data = unsafe.Pointer(&buf[0])
}
n, _, errno := syscall.Syscall6(syscall.SYS_EXTATTR_LIST_FILE,
uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
extattrNamespaceUser,
uintptr(data),
uintptr(len(buf)),
0, 0)
if errno != 0 {
return 0, errno
}
return int(n), nil
}
// extattrDeleteFile takes the attribute attr off path, extattr(2).
func extattrDeleteFile(path, attr string) error {
name, err := syscall.ByteSliceFromString(attr)
if err != nil {
return err
}
_, _, errno := syscall.Syscall(syscall.SYS_EXTATTR_DELETE_FILE,
uintptr(unsafe.Pointer(syscall.StringBytePtr(path))),
extattrNamespaceUser,
uintptr(unsafe.Pointer(&name[0])))
if errno != 0 {
return errno
}
return nil
}