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

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-21 18:51:17 +02:00
commit a9b8039ef7
153 changed files with 34403 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4server
import (
"testing"
"time"
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
)
func TestStateStoreDelegOfAndGrace(t *testing.T) {
s := newStateStore("")
fh := nfsfs.Handle([]byte("delegated-file"))
st, status := s.open(fh, 7, []byte("owner"), nfs4.ShareAccessRead, 0)
if status != nfs4.ErrOK {
t.Fatalf("open: %d", status)
}
if _, ok := s.delegOf(st); ok {
t.Fatal("no delegation bound yet")
}
deleg := nfs4.Stateid{1, 'D', 'E', 'L'}
s.bindDelegation(st, deleg)
got, ok := s.delegOf(st)
if !ok || got != deleg {
t.Fatalf("delegOf %v %v", got, ok)
}
// Only the owning client may close or downgrade the open; a foreign
// stateid use is BAD_STATEID even when the stateid itself is live.
if _, status := s.close(st, 8); status != nfs4.ErrBadStateid {
t.Fatalf("a foreign close: %d", status)
}
if _, status := s.downgrade(st, 8, nfs4.ShareAccessRead, 0); status != nfs4.ErrBadStateid {
t.Fatalf("a foreign downgrade: %d", status)
}
if _, status := s.checkStateid(st, fh, 8); status != nfs4.ErrBadStateid {
t.Fatalf("a foreign stateid check: %d", status)
}
if _, status := s.close(st, 7); status != nfs4.ErrOK {
t.Fatalf("the owner's close: %d", status)
}
// The grace window: a reclaim inside the window passes once, the
// second announcement and every reclaim after it fail with the
// error that names the reason.
g := newGrace(time.Minute)
if !g.active(time.Now()) {
t.Fatal("grace inactive at birth")
}
if ok, status := g.reclaimOKFor(9, time.Now()); !ok || status != 0 {
t.Fatalf("reclaim refused inside the window: %d", status)
}
if !g.complete(9, time.Now()) {
t.Fatal("first complete refused")
}
if g.complete(9, time.Now()) {
t.Fatal("second complete accepted")
}
if ok, status := g.reclaimOKFor(9, time.Now()); ok || status != nfs4.ErrCompleteAlready {
t.Fatalf("done client still reclaimable: %d", status)
}
if ok, status := g.reclaimOKFor(9, time.Now().Add(time.Hour)); ok || status != nfs4.ErrNoGrace {
t.Fatalf("reclaim accepted after the window: %d", status)
}
// FREE_STATEID removes the owner state the byOwn index points at,
// and only for the owner.
locks := newLockStore()
lst, status := locks.lock(fh, 5, []byte("lock-owner"), true, 0, 10)
if status != nfs4.ErrOK {
t.Fatalf("lock: %d", status)
}
if status := locks.freeStateid(lst, 6); status != nfs4.ErrBadStateid {
t.Fatalf("a foreign free: %d", status)
}
if status := locks.freeStateid(lst, 5); status != nfs4.ErrLocksHeld {
t.Fatalf("free with ranges: %d", status)
}
if _, status := locks.unlock(lst, 6, 0, 0); status != nfs4.ErrBadStateid {
t.Fatalf("a foreign unlock: %d", status)
}
if _, status := locks.unlock(lst, 5, 0, 0); status != nfs4.ErrOK {
t.Fatalf("unlock: %d", status)
}
if status := locks.freeStateid(lst, 5); status != nfs4.ErrOK {
t.Fatalf("free: %d", status)
}
locks.releaseOwner(5, []byte("lock-owner"))
}
// Re-locking a range the owner already holds merges into one range
// instead of piling entries up, RFC 8881 section 18.10.
func TestLockRangesMerge(t *testing.T) {
locks := newLockStore()
fh := nfsfs.Handle([]byte("merge-file"))
if _, status := locks.lock(fh, 1, []byte("o"), true, 0, 10); status != nfs4.ErrOK {
t.Fatalf("first lock: %d", status)
}
if _, status := locks.lock(fh, 1, []byte("o"), true, 10, 10); status != nfs4.ErrOK {
t.Fatalf("second lock: %d", status)
}
if _, status := locks.lock(fh, 1, []byte("o"), true, 20, 10); status != nfs4.ErrOK {
t.Fatalf("third lock: %d", status)
}
// The merged span answers conflicts as one range, 0 to 30.
denied, dstatus := locks.test(fh, 2, []byte("other"), true, 5, 1)
if dstatus != nfs4.ErrDenied || denied.Offset != 0 || denied.Length != 30 {
t.Fatalf("the merged range answers conflicts as one: %+v %d", denied, dstatus)
}
// A read lock inside the write span carves its bytes out of the write
// form: a read probe over the carved bytes passes, one over the
// neighbouring write bytes still conflicts.
if _, status := locks.lock(fh, 1, []byte("o"), false, 5, 1); status != nfs4.ErrOK {
t.Fatalf("read inside write: %d", status)
}
if _, dstatus := locks.test(fh, 2, []byte("other"), false, 5, 1); dstatus != nfs4.ErrOK {
t.Fatalf("a read probe over the carved read range conflicted: %d", dstatus)
}
if _, dstatus := locks.test(fh, 2, []byte("other"), false, 6, 1); dstatus != nfs4.ErrDenied {
t.Fatalf("a read probe over the neighbouring write range passed: %d", dstatus)
}
}