// Copyright (c) 2026 Petr BalvĂ­n (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 }