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,383 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nfs4server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
|
||||
)
|
||||
|
||||
func TestOps42Matrix(t *testing.T) {
|
||||
h := testTree(t)
|
||||
sid, seq := newSession(t, h)
|
||||
run := func(s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
||||
t.Helper()
|
||||
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...)
|
||||
payload := nfs4.AppendCompoundArgs(nil, "ops42", nfs4.MinorVersion, all)
|
||||
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
||||
if !ok {
|
||||
t.Fatal("garbage")
|
||||
}
|
||||
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
||||
if derr != nil {
|
||||
t.Fatalf("decode: %v", derr)
|
||||
}
|
||||
return r, bodies[1:]
|
||||
}
|
||||
|
||||
// Open the source file and create the copy targets.
|
||||
seq++
|
||||
res, bodies := run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendOpenArgs(nil, 0x5555, []byte("c42"), nfs4.ShareAccessBoth, 0, false, 0, "a.txt"),
|
||||
})
|
||||
wantStatus(t, "open src", res.Status, nfs4.ErrOK)
|
||||
srcSt := bodyStateid(bodies[1])
|
||||
seq++
|
||||
res, bodies = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendOpenArgs(nil, 0x5555, []byte("c42"), nfs4.ShareAccessBoth, 0, true, 0o644, "copy1"),
|
||||
nfs4.AppendGetfh(nil),
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendOpenArgs(nil, 0x5555, []byte("c42"), nfs4.ShareAccessBoth, 0, true, 0o644, "clone1"),
|
||||
nfs4.AppendGetfh(nil),
|
||||
})
|
||||
wantStatus(t, "open targets", res.Status, nfs4.ErrOK)
|
||||
copySt := bodyStateid(bodies[1])
|
||||
copyFH, ferr := xdr.NewDecoder(bodies[2]).VarOpaque()
|
||||
if ferr != nil {
|
||||
t.Fatal(ferr)
|
||||
}
|
||||
cloneSt := bodyStateid(bodies[4])
|
||||
cloneFH, ferr := xdr.NewDecoder(bodies[5]).VarOpaque()
|
||||
if ferr != nil {
|
||||
t.Fatal(ferr)
|
||||
}
|
||||
|
||||
// COPY: saved fh is the source, current fh the destination.
|
||||
seq++
|
||||
res, bodies = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSavefh(nil),
|
||||
nfs4.AppendPutfh(nil, copyFH),
|
||||
nfs4.AppendCopyArgs(nil, srcSt, copySt, 0, 0, 6, true, true, nil),
|
||||
})
|
||||
wantStatus(t, "copy", res.Status, nfs4.ErrOK)
|
||||
cd := xdr.NewDecoder(bodies[4])
|
||||
if n, cerr := cd.Uint32(); cerr != nil || n != 0 {
|
||||
t.Fatalf("callback array %d: %v", n, cerr)
|
||||
}
|
||||
if count, cerr := cd.Uint64(); cerr != nil || count != 6 {
|
||||
t.Fatalf("copied %d: %v", count, cerr)
|
||||
}
|
||||
data, rerr := h.FS.Read(copyFH, 0, 32)
|
||||
if rerr != nil || !bytes.Equal(data, []byte("hello ")) {
|
||||
t.Fatalf("copy content %q: %v", data, rerr)
|
||||
}
|
||||
|
||||
// CLONE: the whole source into the target, ranges must fit.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSavefh(nil),
|
||||
nfs4.AppendPutfh(nil, cloneFH),
|
||||
nfs4.AppendCloneArgs(nil, srcSt, cloneSt, 0, 0, 1000),
|
||||
})
|
||||
if res.Status != nfs4.ErrInval {
|
||||
t.Fatalf("clone beyond size: status %d", res.Status)
|
||||
}
|
||||
// The clone target must be pre-extended to the range size, RFC 7862
|
||||
// section 15.1.4.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutfh(nil, cloneFH),
|
||||
nfs4.AppendSetattrArgs(nil, nfs4.Stateid{}, nfs4.OfBits(nfs4.AttrSize), nfs4.Attrs{Size: 9}),
|
||||
})
|
||||
wantStatus(t, "extend clone target", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSavefh(nil),
|
||||
nfs4.AppendPutfh(nil, cloneFH),
|
||||
nfs4.AppendCloneArgs(nil, srcSt, cloneSt, 0, 0, 9),
|
||||
})
|
||||
wantStatus(t, "clone", res.Status, nfs4.ErrOK)
|
||||
data, rerr = h.FS.Read(cloneFH, 0, 32)
|
||||
if rerr != nil || !bytes.Equal(data, []byte("hello nfs")) {
|
||||
t.Fatalf("clone content %q: %v", data, rerr)
|
||||
}
|
||||
|
||||
// READ_PLUS answers one data segment.
|
||||
seq++
|
||||
res, bodies = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendReadPlusArgs(nil, srcSt, 0, 32),
|
||||
})
|
||||
wantStatus(t, "read plus", res.Status, nfs4.ErrOK)
|
||||
rd := xdr.NewDecoder(bodies[2])
|
||||
if eof, eerr := rd.Bool(); eerr != nil || !eof {
|
||||
t.Fatalf("eof %v: %v", eof, eerr)
|
||||
}
|
||||
if n, _ := rd.Uint32(); n != 1 {
|
||||
t.Fatalf("contents %d", n)
|
||||
}
|
||||
if kind, _ := rd.Uint32(); kind != nfs4.ContentData {
|
||||
t.Fatalf("content kind %d", kind)
|
||||
}
|
||||
if _, err := rd.Uint64(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pdata, perr := rd.VarOpaque()
|
||||
if perr != nil || !bytes.Equal(pdata, []byte("hello nfs")) {
|
||||
t.Fatalf("read plus data %q: %v", pdata, perr)
|
||||
}
|
||||
|
||||
// IO_ADVISE echoes the hints; WRITE_SAME repeats the pattern.
|
||||
hints := nfs4.Bitmap{}.With(1)
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "copy1"),
|
||||
nfs4.AppendIoAdviseArgs(nil, copySt, 0, 100, hints),
|
||||
})
|
||||
wantStatus(t, "io advise", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "copy1"),
|
||||
nfs4.AppendWriteSameArgs(nil, copySt, nfs4.StableFileSync, 0, 4, 3, 0, 0, 0, []byte("ab")),
|
||||
})
|
||||
wantStatus(t, "write same", res.Status, nfs4.ErrOK)
|
||||
data, rerr = h.FS.Read(copyFH, 0, 12)
|
||||
if rerr != nil || !bytes.Equal(data, []byte("abababababab")) {
|
||||
t.Fatalf("write same content %q: %v", data, rerr)
|
||||
}
|
||||
|
||||
// OFFLOAD_STATUS without a request answers the dedicated status.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "copy1"),
|
||||
nfs4.AppendOffloadStatusArgs(nil, copySt),
|
||||
})
|
||||
wantStatus(t, "offload status", res.Status, nfs4.ErrOffloadNoReqs)
|
||||
// OFFLOAD_CANCEL answers OK.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "copy1"),
|
||||
nfs4.AppendOffloadCancelArgs(nil, copySt),
|
||||
})
|
||||
wantStatus(t, "offload cancel", res.Status, nfs4.ErrOK)
|
||||
// COPY_NOTIFY grants the copy with this server as the source.
|
||||
seq++
|
||||
res, bodies = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendCopyNotifyArgs(nil, srcSt, 3, "", nfs4.NetAddr{Netid: "tcp", Uaddr: "10.0.0.9.8.1"}),
|
||||
})
|
||||
wantStatus(t, "copy notify", res.Status, nfs4.ErrOK)
|
||||
// LAYOUTERROR and LAYOUTSTATS accept the report.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendLayoutErrorArgs(nil, 0, 4096, srcSt, layoutDeviceID, nfs4.ErrBadStateid, nfs4.OpRead),
|
||||
})
|
||||
wantStatus(t, "layout error", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendLayoutStatsArgs(nil, 0, 4096, srcSt, 1, 9, 0, 0, layoutDeviceID, 0, nil),
|
||||
})
|
||||
wantStatus(t, "layout stats", res.Status, nfs4.ErrOK)
|
||||
// SEEK: the backend has no hole in a.txt; a data seek finds offset 0
|
||||
// and a hole seek past the end answers NOSPC.
|
||||
seq++
|
||||
res, bodies = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSeekArgs(nil, srcSt, 3, nfs4.ContentData),
|
||||
})
|
||||
wantStatus(t, "seek data", res.Status, nfs4.ErrOK)
|
||||
sd := xdr.NewDecoder(bodies[2])
|
||||
if eof, _ := sd.Bool(); eof {
|
||||
t.Fatal("seek data reported eof")
|
||||
}
|
||||
if off, _ := sd.Uint64(); off != 3 {
|
||||
t.Fatalf("seek offset %d", off)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOps42RangesAndEdges(t *testing.T) {
|
||||
h := testTree(t)
|
||||
sid, seq := newSession(t, h)
|
||||
run := func(s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
||||
t.Helper()
|
||||
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...)
|
||||
payload := nfs4.AppendCompoundArgs(nil, "rng", nfs4.MinorVersion, all)
|
||||
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
||||
if !ok {
|
||||
t.Fatal("garbage")
|
||||
}
|
||||
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
||||
if derr != nil {
|
||||
t.Fatalf("decode: %v", derr)
|
||||
}
|
||||
return r, bodies[1:]
|
||||
}
|
||||
|
||||
seq++
|
||||
res, bodies := run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendOpenArgs(nil, 0x6666, []byte("rng"), nfs4.ShareAccessBoth, 0, false, 0, "a.txt"),
|
||||
})
|
||||
wantStatus(t, "open", res.Status, nfs4.ErrOK)
|
||||
st := bodyStateid(bodies[1])
|
||||
|
||||
// ALLOCATE reserves, DEALLOCATE releases; a zero length is refused.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendAllocateArgs(nil, st, 0, 64),
|
||||
})
|
||||
wantStatus(t, "allocate", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendDeallocateArgs(nil, st, 0, 64),
|
||||
})
|
||||
wantStatus(t, "deallocate", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendDeallocateArgs(nil, st, 0, 0),
|
||||
})
|
||||
wantStatus(t, "deallocate zero", res.Status, nfs4.ErrInval)
|
||||
|
||||
// SEEK with a nonsense content kind answers INVAL.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSeekArgs(nil, st, 0, 9),
|
||||
})
|
||||
wantStatus(t, "seek nonsense", res.Status, nfs4.ErrInval)
|
||||
|
||||
// COPY with named source servers decodes the netloc list.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSavefh(nil),
|
||||
nfs4.AppendPutfh(nil, mustLookup(t, h, "a.txt")),
|
||||
nfs4.AppendCopyArgs(nil, st, nfs4.Stateid{}, 0, 0, 0, false, true,
|
||||
[]nfs4.CopySourceServer{{Type: 1, Name: "src.example.org"}}),
|
||||
})
|
||||
wantStatus(t, "copy zero length", res.Status, nfs4.ErrOK)
|
||||
|
||||
// REMOVEXATTR of a missing attribute answers NOXATTR.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendRemoveXattrArgs(nil, "user.nothere"),
|
||||
})
|
||||
wantStatus(t, "remove missing xattr", res.Status, nfs4.ErrNoXattr)
|
||||
}
|
||||
|
||||
func TestOps42WithoutFileHandle(t *testing.T) {
|
||||
h := testTree(t)
|
||||
sid, seq := newSession(t, h)
|
||||
// Every 4.2 data operation refuses to run without a current file
|
||||
// handle; a table keeps the coverage of the guard honest.
|
||||
table := []struct {
|
||||
name string
|
||||
op []byte
|
||||
}{
|
||||
{"seek", nfs4.AppendSeekArgs(nil, nfs4.Stateid{}, 0, nfs4.ContentData)},
|
||||
{"allocate", nfs4.AppendAllocateArgs(nil, nfs4.Stateid{}, 0, 8)},
|
||||
{"deallocate", nfs4.AppendDeallocateArgs(nil, nfs4.Stateid{}, 0, 8)},
|
||||
{"ioadvise", nfs4.AppendIoAdviseArgs(nil, nfs4.Stateid{}, 0, 8, nfs4.Bitmap{})},
|
||||
{"copy", nfs4.AppendCopyArgs(nil, nfs4.Stateid{}, nfs4.Stateid{}, 0, 0, 0, false, true, nil)},
|
||||
{"copynotify", nfs4.AppendCopyNotifyArgs(nil, nfs4.Stateid{}, 2, "dst", nfs4.NetAddr{})},
|
||||
{"offloadcancel", nfs4.AppendOffloadCancelArgs(nil, nfs4.Stateid{})},
|
||||
{"offloadstatus", nfs4.AppendOffloadStatusArgs(nil, nfs4.Stateid{})},
|
||||
{"layouterror", nfs4.AppendLayoutErrorArgs(nil, 0, 0, nfs4.Stateid{}, layoutDeviceID, 0, 0)},
|
||||
{"layoutstats", nfs4.AppendLayoutStatsArgs(nil, 0, 0, nfs4.Stateid{}, 0, 0, 0, 0, layoutDeviceID, 0, nil)},
|
||||
{"readplus", nfs4.AppendReadPlusArgs(nil, nfs4.Stateid{}, 0, 8)},
|
||||
{"writesame", nfs4.AppendWriteSameArgs(nil, nfs4.Stateid{}, nfs4.StableFileSync, 0, 8, 1, 0, 0, 0, []byte("x"))},
|
||||
}
|
||||
for _, row := range table {
|
||||
seq++
|
||||
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true)}, row.op)
|
||||
payload := nfs4.AppendCompoundArgs(nil, "no-fh", nfs4.MinorVersion, all)
|
||||
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
||||
if !ok {
|
||||
t.Fatalf("%s: garbage", row.name)
|
||||
}
|
||||
res, _, err := nfs4.DecodeCompoundResBodies(body)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: decode %v", row.name, err)
|
||||
}
|
||||
if res.Status != nfs4.ErrNoFileHandle {
|
||||
t.Fatalf("%s: status %d, want NOFILE_HANDLE", row.name, res.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeekUnderLockStateid(t *testing.T) {
|
||||
h := testTree(t)
|
||||
sid, seq := newSession(t, h)
|
||||
run := func(s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) {
|
||||
t.Helper()
|
||||
all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...)
|
||||
payload := nfs4.AppendCompoundArgs(nil, "lockseek", nfs4.MinorVersion, all)
|
||||
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
||||
if !ok {
|
||||
t.Fatal("garbage")
|
||||
}
|
||||
r, bodies, derr := nfs4.DecodeCompoundResBodies(body)
|
||||
if derr != nil {
|
||||
t.Fatalf("decode: %v", derr)
|
||||
}
|
||||
return r, bodies[1:]
|
||||
}
|
||||
seq++
|
||||
res, bodies := run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendOpenArgs(nil, 0x7777, []byte("ls"), nfs4.ShareAccessBoth, 0, false, 0, "a.txt"),
|
||||
})
|
||||
wantStatus(t, "open", res.Status, nfs4.ErrOK)
|
||||
openSt := bodyStateid(bodies[1])
|
||||
seq++
|
||||
res, bodies = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendLockArgsNew(nil, openSt, 0x7777, []byte("locker"),
|
||||
nfs4.LockTypeWrite, false, 0, 9),
|
||||
})
|
||||
wantStatus(t, "lock", res.Status, nfs4.ErrOK)
|
||||
lockSt := bodyStateid(bodies[2])
|
||||
// A data operation under the LOCK stateid is legal; the store falls
|
||||
// back from the open store to the lock store.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSeekArgs(nil, lockSt, 0, nfs4.ContentData),
|
||||
})
|
||||
wantStatus(t, "seek under lock", res.Status, nfs4.ErrOK)
|
||||
}
|
||||
Reference in New Issue
Block a user