// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT // Directory delegations: the store of granted directory delegations, the // GET_DIR_DELEGATION handler and the notification the server pushes to a // deleg holder when the directory changes under it. package nfs4server import ( "sync" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/nfsfs" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // A dirDeleg is one granted directory delegation. type dirDeleg struct { stateid nfs4.Stateid sessID nfs4.SessionID clientid uint64 fh nfsfs.Handle types nfs4.Bitmap } // dirDelegStore tracks the live directory delegations. A directory // carries at most one. type dirDelegStore struct { mu sync.Mutex next uint64 byDir map[string]*dirDeleg } func newDirDelegStore() *dirDelegStore { return &dirDelegStore{byDir: make(map[string]*dirDeleg)} } // grant issues the directory delegation; a second client asking for the // same directory is denied, the same client receives its live stateid. func (s *dirDelegStore) grant(sessID nfs4.SessionID, clientid uint64, fh nfsfs.Handle) (*dirDeleg, uint32) { s.mu.Lock() defer s.mu.Unlock() if d, ok := s.byDir[fileKey(fh)]; ok { if d.clientid == clientid { return d, nfs4.ErrOK } return nil, nfs4.ErrDenied } s.next++ var st nfs4.Stateid setStateidSeq(&st, 1) copy(st[4:], "DDEL") for i := range 8 { st[15-i] = byte(s.next >> (8 * i)) } d := &dirDeleg{stateid: st, sessID: sessID, clientid: clientid, fh: fh} s.byDir[fileKey(fh)] = d return d, nfs4.ErrOK } // dropClient and dropSession remove the delegations that die with the // client or the session. func (s *dirDelegStore) dropClient(clientid uint64) { s.mu.Lock() defer s.mu.Unlock() for key, d := range s.byDir { if d.clientid == clientid { delete(s.byDir, key) } } } func (s *dirDelegStore) dropSession(sessID nfs4.SessionID) { s.mu.Lock() defer s.mu.Unlock() for key, d := range s.byDir { if d.sessID == sessID { delete(s.byDir, key) } } } // count reports how many directory delegations are live. func (s *dirDelegStore) count() int { s.mu.Lock() defer s.mu.Unlock() return len(s.byDir) } // dirDelegs returns the directory delegation store, made once per // handler. func (h *Handler) dirDelegs() *dirDelegStore { h.mu.Lock() defer h.mu.Unlock() if h.dirDelegSt == nil { h.dirDelegSt = newDirDelegStore() } return h.dirDelegSt } // getDirDelegationOp serves GET_DIR_DELEGATION over the current // directory: the stateid hangs from an open of the directory and the // notification types are echoed back, this server can produce them all. func (h *Handler) getDirDelegationOp(d *xdr.Decoder, reg *fhreg, sessID nfs4.SessionID, clientid uint64) ([]byte, uint32, error) { signalAvail, err := d.Bool() if err != nil { return nil, 0, err } notifyTypes, err := nfs4.ReadBitmap(d) if err != nil { return nil, 0, err } for range 2 { if _, err = d.Int64(); err != nil { // attr delay seconds return nil, 0, err } if _, err = d.Uint32(); err != nil { // attr delay nseconds return nil, 0, err } } if _, err = nfs4.ReadBitmap(d); err != nil { // child attributes return nil, 0, err } if _, err = nfs4.ReadBitmap(d); err != nil { // dir attributes return nil, 0, err } _ = signalAvail if !reg.haveCur { return nil, nfs4.ErrNoFileHandle, nil } // Only directories carry directory delegations. if info, gerr := h.FS.Getattr(reg.cur); gerr == nil && !info.IsDir() { return nil, nfs4.ErrWrongType, nil } dg, status := h.dirDelegs().grant(sessID, clientid, reg.cur) if status != nfs4.ErrOK { return nil, status, nil } verf := h.writeVerifier() return nfs4.AppendGetDirDelegationResOK(nil, verf, dg.stateid, notifyTypes, nfs4.Bitmap{}, nfs4.Bitmap{}), nfs4.ErrOK, nil } // notifyDir pushes one CB_NOTIFY to the holder of the directory // delegation, if the changed directory is delegated to somebody else. // The notification is advisory and queued onto the holder's callback // worker, so a mutation served on the holder's own connection never // waits for the reply only that connection's read loop can route. func (h *Handler) notifyDir(dir nfsfs.Handle, change nfs4.Notify4) { dd, ok := h.dirDelegByFH(dir) if !ok { return } args := nfs4.AppendCBNotifyArgs(nil, dd.stateid, dir, []nfs4.Notify4{change}) _ = h.queueCB(dd.sessID, "dir-notify", [][]byte{args}) } // dirDelegByFH finds the live delegation of a directory. func (h *Handler) dirDelegByFH(dir nfsfs.Handle) (*dirDeleg, bool) { h.mu.Lock() store := h.dirDelegSt h.mu.Unlock() if store == nil { return nil, false } store.mu.Lock() defer store.mu.Unlock() d, ok := store.byDir[fileKey(dir)] return d, ok } // notifyDirOf is the helper the mutating operations call: it reads the // parent through the backend and pushes the change. nameCookie carries // the cookie semantics of the event; zero suffices for the wire shape // this build answers with. func (h *Handler) notifyDirOf(dir nfsfs.Handle, mask nfs4.Bitmap, name string) { vals := buildNotifyBody(mask, name) if vals == nil { return } h.notifyDir(dir, nfs4.Notify4{Mask: mask, Vals: vals}) } // buildNotifyBody encodes the change body the mask names. func buildNotifyBody(mask nfs4.Bitmap, name string) []byte { if mask.Has(nfs4.NotifyAddEntry) { return nfs4.AppendNotifyAdd(nil, name, 0, true) } if mask.Has(nfs4.NotifyRemoveEntry) { return nfs4.AppendNotifyRemove(nil, name, 0) } return nil } // notifyAppendCB walks the decoded notify body for the tests: it splits // the name out of a notify_entry4 payload. func notifyNameOf(body []byte) string { d := xdr.NewDecoder(body) if _, err := d.Uint32(); err != nil { // nad_old_entry count return "" } name, err := d.String() if err != nil { return "" } return name }