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
355 lines
11 KiB
Go
355 lines
11 KiB
Go
// 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 TestOps41Matrix(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, "ops41", 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:]
|
|
}
|
|
|
|
// LOOKUPP walks to the parent: the root is its own parent.
|
|
var rootFH []byte
|
|
res, bodies := run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendGetfh(nil),
|
|
})
|
|
rootFH, err := xdr.NewDecoder(bodies[1]).VarOpaque()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seq++
|
|
res, bodies = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "sub"),
|
|
nfs4.AppendLookupp(nil),
|
|
nfs4.AppendGetfh(nil),
|
|
})
|
|
if res.Status != nfs4.ErrOK {
|
|
t.Fatalf("lookupp: status %d", res.Status)
|
|
}
|
|
got, derr := xdr.NewDecoder(bodies[3]).VarOpaque()
|
|
if derr != nil || !bytes.Equal(got, rootFH) {
|
|
t.Fatalf("lookupp landed outside the root: % x %v", got, derr)
|
|
}
|
|
|
|
// PUTPUBFH answers the same handle as PUTROOTFH.
|
|
seq++
|
|
res, bodies = run(seq, [][]byte{
|
|
nfs4.AppendPutPubfh(nil),
|
|
nfs4.AppendGetfh(nil),
|
|
})
|
|
wantStatus(t, "putpubfh", res.Status, nfs4.ErrOK)
|
|
pub, derr := xdr.NewDecoder(bodies[1]).VarOpaque()
|
|
if derr != nil || !bytes.Equal(pub, rootFH) {
|
|
t.Fatalf("public handle differs from the root")
|
|
}
|
|
|
|
// VERIFY passes when the file matches and fails with NOT_SAME when it
|
|
// does not; NVERIFY is the exact opposite.
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendVerifyArgs(nil, nfs4.OfBits(nfs4.AttrMode, nfs4.AttrSize),
|
|
nfs4.Attrs{Mode: 0o644, Size: 9}),
|
|
})
|
|
wantStatus(t, "verify pass", res.Status, nfs4.ErrOK)
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendVerifyArgs(nil, nfs4.OfBits(nfs4.AttrSize), nfs4.Attrs{Size: 8}),
|
|
})
|
|
wantStatus(t, "verify mismatch", res.Status, nfs4.ErrNotSame)
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendNverifyArgs(nil, nfs4.OfBits(nfs4.AttrSize), nfs4.Attrs{Size: 8}),
|
|
})
|
|
wantStatus(t, "nverify pass", res.Status, nfs4.ErrOK)
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendNverifyArgs(nil, nfs4.OfBits(nfs4.AttrSize), nfs4.Attrs{Size: 9}),
|
|
})
|
|
wantStatus(t, "nverify match", res.Status, nfs4.ErrSame)
|
|
|
|
// Open the file for the stateful half of the matrix.
|
|
seq++
|
|
res, bodies = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0x3333, []byte("ops41-owner"),
|
|
nfs4.ShareAccessBoth, 0, false, 0, "a.txt"),
|
|
})
|
|
wantStatus(t, "open", res.Status, nfs4.ErrOK)
|
|
var openSt nfs4.Stateid
|
|
copy(openSt[:], bodies[1])
|
|
var delegSt nfs4.Stateid
|
|
if len(bodies[1]) >= 68 {
|
|
copy(delegSt[:], bodies[1][52:68])
|
|
}
|
|
|
|
// DELEGRETURN hands the delegation back; a second return is answered
|
|
// OK the same way, the release of nothing is already done.
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutfh(nil, []byte("ignored")),
|
|
nfs4.AppendDelegReturnArgs(nil, delegSt),
|
|
})
|
|
if res.Status != nfs4.ErrOK && res.Status != nfs4.ErrBadStateid {
|
|
t.Fatalf("deleg return: status %d", res.Status)
|
|
}
|
|
if res.Status == nfs4.ErrOK {
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendDelegReturnArgs(nil, delegSt),
|
|
})
|
|
wantStatus(t, "deleg return again", res.Status, nfs4.ErrOK)
|
|
}
|
|
|
|
// A byte range lock gives the lock owner a stateid; LOCKT of a second
|
|
// owner sees it, RELEASE_LOCKOWNER removes it.
|
|
seq++
|
|
res, bodies = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendLockArgsNew(nil, openSt, 0x3333, []byte("ops41-locker"),
|
|
nfs4.LockTypeWrite, false, 0, 10),
|
|
})
|
|
wantStatus(t, "lock", res.Status, nfs4.ErrOK)
|
|
var lockSt nfs4.Stateid
|
|
copy(lockSt[:], bodies[2])
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendLocktArgs(nil, nfs4.LockTypeWrite, 0, 10, 0x9999, []byte("other")),
|
|
})
|
|
wantStatus(t, "lockt sees the lock", res.Status, nfs4.ErrDenied)
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendReleaseLockOwnerArgs(nil, sid.ClientIDOf(), []byte("ops41-locker")),
|
|
})
|
|
wantStatus(t, "release lock owner", res.Status, nfs4.ErrOK)
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendLocktArgs(nil, nfs4.LockTypeWrite, 0, 10, 0x9999, []byte("other")),
|
|
})
|
|
wantStatus(t, "lockt after release", res.Status, nfs4.ErrOK)
|
|
|
|
// FREE_STATEID refuses a stateid that never existed and the one that
|
|
// was just dropped.
|
|
seq++
|
|
res, _ = run(seq, [][]byte{nfs4.AppendFreeStateidArgs(nil, lockSt)})
|
|
if res.Status != nfs4.ErrOK && res.Status != nfs4.ErrBadStateid {
|
|
t.Fatalf("free stateid: status %d", res.Status)
|
|
}
|
|
seq++
|
|
res, _ = run(seq, [][]byte{nfs4.AppendFreeStateidArgs(nil, lockSt)})
|
|
wantStatus(t, "free stateid again", res.Status, nfs4.ErrBadStateid)
|
|
|
|
// TEST_STATEID answers one status per stateid: the live open, a dead
|
|
// stateid and the anonymous form.
|
|
seq++
|
|
res, bodies = run(seq, [][]byte{
|
|
nfs4.AppendTestStateidArgs(nil, []nfs4.Stateid{openSt, lockSt, {}}),
|
|
})
|
|
wantStatus(t, "test stateid", res.Status, nfs4.ErrOK)
|
|
td := xdr.NewDecoder(bodies[0])
|
|
count, terr := td.Uint32()
|
|
if terr != nil || count != 3 {
|
|
t.Fatalf("statuses %d: %v", count, terr)
|
|
}
|
|
s0, _ := td.Uint32()
|
|
s1, _ := td.Uint32()
|
|
s2, _ := td.Uint32()
|
|
if s0 != nfs4.ErrOK || s1 != nfs4.ErrBadStateid || s2 != nfs4.ErrOK {
|
|
t.Fatalf("statuses %d %d %d", s0, s1, s2)
|
|
}
|
|
|
|
// BIND_CONN_TO_SESSION refuses a session that does not exist.
|
|
seq++
|
|
badSID := nfs4.SessionID{}
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendBindConnToSessionArgs(nil, badSID, nfs4.Cdfc4Fore, false),
|
|
})
|
|
wantStatus(t, "bind unknown session", res.Status, nfs4.ErrBadSession)
|
|
seq++
|
|
res, bodies = run(seq, [][]byte{
|
|
nfs4.AppendBindConnToSessionArgs(nil, sid, nfs4.Cdfc4Fore, false),
|
|
})
|
|
wantStatus(t, "bind fore", res.Status, nfs4.ErrOK)
|
|
bd := xdr.NewDecoder(bodies[0])
|
|
var echoed nfs4.SessionID
|
|
raw, rerr := bd.Raw(16)
|
|
if rerr != nil {
|
|
t.Fatal(rerr)
|
|
}
|
|
copy(echoed[:], raw)
|
|
if echoed != sid {
|
|
t.Fatal("bind echoed another session")
|
|
}
|
|
if dir, _ := bd.Uint32(); dir != nfs4.Cdfs4Fore {
|
|
t.Fatalf("dir %d", dir)
|
|
}
|
|
if bd.Remaining() != 0 {
|
|
t.Fatalf("%d bytes left in the bind result", bd.Remaining())
|
|
}
|
|
|
|
// BACKCHANNEL_CTL records the future back channel program.
|
|
seq++
|
|
res, _ = run(seq, [][]byte{nfs4.AppendBackchannelCtlArgs(nil, 0x40000001)})
|
|
wantStatus(t, "backchannel ctl", res.Status, nfs4.ErrOK)
|
|
}
|
|
|
|
func TestVerifyTimeAndBareLookupp(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, "verify", 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:]
|
|
}
|
|
|
|
// LOOKUPP without a current handle answers NOFILE_HANDLE.
|
|
seq++
|
|
res, _ := run(seq, [][]byte{nfs4.AppendLookupp(nil)})
|
|
wantStatus(t, "lookupp bare", res.Status, nfs4.ErrNoFileHandle)
|
|
|
|
// The modify time of a.txt asserts against itself: VERIFY passes,
|
|
// NVERIFY answers SAME; a wrong time flips both answers.
|
|
info, err := h.FS.Getattr(mustLookup(t, h, "a.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
modified := nfs4.NfsTime{Seconds: info.ModTime.Unix(), Nseconds: uint32(info.ModTime.Nanosecond())}
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendVerifyArgs(nil, nfs4.OfBits(nfs4.AttrTimeModify),
|
|
nfs4.Attrs{TimeModify: modified}),
|
|
})
|
|
wantStatus(t, "verify time", res.Status, nfs4.ErrOK)
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendNverifyArgs(nil, nfs4.OfBits(nfs4.AttrTimeModify),
|
|
nfs4.Attrs{TimeModify: modified}),
|
|
})
|
|
wantStatus(t, "nverify time match", res.Status, nfs4.ErrSame)
|
|
modified.Nseconds++
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendVerifyArgs(nil, nfs4.OfBits(nfs4.AttrTimeModify),
|
|
nfs4.Attrs{TimeModify: modified}),
|
|
})
|
|
wantStatus(t, "verify time mismatch", res.Status, nfs4.ErrNotSame)
|
|
}
|
|
|
|
func TestOps41EdgeBranches(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, "edge", 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:]
|
|
}
|
|
|
|
// A VERIFY of an attribute this server refuses to compare answers
|
|
// ATTRNOTSUPP.
|
|
seq++
|
|
res, _ := run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendLookup(nil, "a.txt"),
|
|
nfs4.AppendVerifyArgs(nil, nfs4.OfBits(nfs4.AttrMountedOnFileID),
|
|
nfs4.Attrs{MountedOnFileID: 1}),
|
|
})
|
|
wantStatus(t, "verify unsupported attr", res.Status, nfs4.ErrAttrNotSupp)
|
|
|
|
// A nonsense connection direction answers INVAL.
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendBindConnToSessionArgs(nil, sid, 9, false),
|
|
})
|
|
wantStatus(t, "bind direction", res.Status, nfs4.ErrInval)
|
|
|
|
// FREE_STATEID of a lock owner that still holds ranges answers
|
|
// LOCKS_HELD.
|
|
seq++
|
|
res, bodies := run(seq, [][]byte{
|
|
nfs4.AppendPutRootfh(nil),
|
|
nfs4.AppendOpenArgs(nil, 0x4444, []byte("edge-owner"),
|
|
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.AppendPutfh(nil, mustLookup(t, h, "a.txt")),
|
|
nfs4.AppendLockArgsNew(nil, openSt, 0x4444,
|
|
[]byte("edge-locker"), nfs4.LockTypeWrite, false, 0, 10),
|
|
})
|
|
wantStatus(t, "lock", res.Status, nfs4.ErrOK)
|
|
lockSt := bodyStateid(bodies[1])
|
|
seq++
|
|
res, _ = run(seq, [][]byte{
|
|
nfs4.AppendFreeStateidArgs(nil, lockSt),
|
|
})
|
|
wantStatus(t, "free with locks held", res.Status, nfs4.ErrLocksHeld)
|
|
}
|
|
|
|
// bodyStateid reads the stateid that opens every stateful result body.
|
|
func bodyStateid(body []byte) nfs4.Stateid {
|
|
var st nfs4.Stateid
|
|
copy(st[:], body)
|
|
return st
|
|
}
|