feat: full NFSv4.2 server and client in pure Go
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
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
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user