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

224 lines
6.0 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Named attributes of RFC 8881 section 5.3: the OPENATTR operation
// opens a synthetic directory of an object's extended attributes, and
// the file operations over the synthetic handles map to the backend's
// xattr half.
package nfs4server
import (
"strings"
"sync"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
)
// The synthetic handle prefixes of the named attribute space.
const (
nattrDirPrefix = "NAD/" // the named attribute directory of an object
nattrFilePrefix = "NAF/" // one named attribute of an object
)
// nattrStore keeps the mapping between synthetic handles and the object
// they hang from, so a named attribute file remembers its parent.
type nattrStore struct {
mu sync.Mutex
dirs map[string]nfsfs.Handle // parent object -> dir handle
}
func newNattrStore() *nattrStore {
return &nattrStore{dirs: make(map[string]nfsfs.Handle)}
}
func (s *nattrStore) dirFor(parent nfsfs.Handle) nfsfs.Handle {
s.mu.Lock()
defer s.mu.Unlock()
if d, ok := s.dirs[string(parent)]; ok {
return d
}
d := nfsfs.Handle(append([]byte(nattrDirPrefix), parent...))
s.dirs[string(parent)] = d
return d
}
// dirOf pulls the directory handle and the attribute name out of a
// synthetic file handle.
func (s *nattrStore) fileFor(dir, name string) nfsfs.Handle {
s.mu.Lock()
defer s.mu.Unlock()
return nfsfs.Handle(nattrFilePrefix + name + "/" + dir)
}
func (s *nattrStore) dirOf(file nfsfs.Handle) (nfsfs.Handle, string, bool) {
text := string(file)
if !strings.HasPrefix(text, nattrFilePrefix) {
return nil, "", false
}
text = text[len(nattrFilePrefix):]
slash := strings.IndexByte(text, '/')
if slash < 0 {
return nil, "", false
}
return nfsfs.Handle(text[slash+1:]), text[:slash], true
}
// parentOf pulls the parent object out of a synthetic directory handle.
func (s *nattrStore) parentOf(dir nfsfs.Handle) (nfsfs.Handle, bool) {
text := string(dir)
if !strings.HasPrefix(text, nattrDirPrefix) {
return nil, false
}
return nfsfs.Handle(text[len(nattrDirPrefix):]), true
}
// nattrs returns the named attribute store, made once per handler.
func (h *Handler) nattrs() *nattrStore {
h.mu.Lock()
defer h.mu.Unlock()
if h.nattrSt == nil {
h.nattrSt = newNattrStore()
}
return h.nattrSt
}
// openattrOp serves OPENATTR: the current object's named attribute
// directory answers as a synthetic handle, RFC 8881 section 18.16.
func (h *Handler) openattrOp(d *xdr.Decoder, reg *fhreg) ([]byte, uint32, error) {
if _, err := d.Bool(); err != nil { // created
return nil, 0, err
}
if !reg.haveCur {
return nil, nfs4.ErrNoFileHandle, nil
}
x, _ := h.FS.(nfsfs.XattrFS)
if x == nil {
return nil, nfs4.ErrNotSupp, nil
}
reg.cur, reg.haveCur = h.nattrs().dirFor(reg.cur), true
return nil, nfs4.ErrOK, nil
}
// isNattrDir reports whether the handle names a named attribute
// directory.
func (h *Handler) isNattrDir(fh nfsfs.Handle) bool {
_, ok := h.nattrs().parentOf(fh)
return ok
}
// isNattrFile reports whether the handle names a named attribute file.
func (h *Handler) isNattrFile(fh nfsfs.Handle) bool {
_, _, ok := h.nattrs().dirOf(fh)
return ok
}
// nattrLookup resolves one attribute name inside the synthetic
// directory; the attribute must exist.
func (h *Handler) nattrLookup(dir nfsfs.Handle, name string) (nfsfs.Handle, uint32) {
parent, ok := h.nattrs().parentOf(dir)
if !ok {
return nil, nfs4.ErrBadHandle
}
x, _ := h.FS.(nfsfs.XattrFS)
if x == nil {
return nil, nfs4.ErrNotSupp
}
if _, err := x.GetXattr(parent, name, 0); err == nfsfs.ErrNoXattr {
return nil, nfs4.ErrNoEnt
} else if err != nil && err != nfsfs.ErrXattrNotSupp {
return nil, nfs4.ErrNoEnt
}
return h.nattrs().fileFor(string(dir), name), nfs4.ErrOK
}
// nattrRead reads the value of a named attribute file.
func (h *Handler) nattrRead(fh nfsfs.Handle, offset uint64) ([]byte, uint32) {
dir, name, ok := h.nattrs().dirOf(fh)
if !ok {
return nil, nfs4.ErrBadHandle
}
x, _ := h.FS.(nfsfs.XattrFS)
if x == nil {
return nil, nfs4.ErrNotSupp
}
parent, ok := h.nattrs().parentOf(dir)
if !ok {
return nil, nfs4.ErrBadHandle
}
value, err := x.GetXattr(parent, name, 0)
if err == nfsfs.ErrNoXattr {
return nil, nfs4.ErrNoEnt
}
if err != nil {
return nil, mapErr(err)
}
if offset >= uint64(len(value)) {
return nil, nfs4.ErrOK
}
return value[offset:], nfs4.ErrOK
}
// nattrWrite writes the value of a named attribute file; named
// attributes rewrite whole, so an offset is refused.
func (h *Handler) nattrWrite(fh nfsfs.Handle, offset uint64, data []byte) (uint32, uint32) {
dir, name, ok := h.nattrs().dirOf(fh)
if !ok {
return 0, nfs4.ErrBadHandle
}
x, _ := h.FS.(nfsfs.XattrFS)
if x == nil {
return 0, nfs4.ErrNotSupp
}
parent, ok := h.nattrs().parentOf(dir)
if !ok {
return 0, nfs4.ErrBadHandle
}
if offset != 0 {
return 0, nfs4.ErrInval
}
if err := x.SetXattr(parent, name, data, nfsfs.XattrModeReplace); err != nil {
return 0, mapErr(err)
}
return uint32(len(data)), nfs4.ErrOK
}
// nattrCreate makes one named attribute with its initial value, the
// CREATE of the synthetic directory.
func (h *Handler) nattrCreate(dir nfsfs.Handle, name string, value []byte) uint32 {
parent, ok := h.nattrs().parentOf(dir)
if !ok {
return nfs4.ErrBadHandle
}
x, _ := h.FS.(nfsfs.XattrFS)
if x == nil {
return nfs4.ErrNotSupp
}
if err := x.SetXattr(parent, name, value, nfsfs.XattrModeCreate); err != nil {
return mapErr(err)
}
return nfs4.ErrOK
}
// nattrRemove deletes one named attribute of the synthetic directory.
func (h *Handler) nattrRemove(dir nfsfs.Handle, name string) uint32 {
parent, ok := h.nattrs().parentOf(dir)
if !ok {
return nfs4.ErrBadHandle
}
x, _ := h.FS.(nfsfs.XattrFS)
if x == nil {
return nfs4.ErrNotSupp
}
if err := x.RemoveXattr(parent, name); err != nil {
if err == nfsfs.ErrNoXattr {
return nfs4.ErrNoEnt
}
return mapErr(err)
}
return nfs4.ErrOK
}