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
318 lines
11 KiB
Go
318 lines
11 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package nfs4
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
|
|
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
|
|
)
|
|
|
|
func TestSessionIDShape(t *testing.T) {
|
|
// The session id is sixteen bytes on the wire, RFC 7863: an eight
|
|
// byte server prefix and the eight byte client id.
|
|
var prefix [8]byte
|
|
copy(prefix[:], []byte("nfs-srv-"))
|
|
id := MakeSessionID(prefix, 0xdeadbeefcafe)
|
|
if len(id) != 16 {
|
|
t.Fatalf("session id of %d bytes, want 16", len(id))
|
|
}
|
|
if id.ClientIDOf() != 0xdeadbeefcafe {
|
|
t.Fatalf("client id half: %x", id.ClientIDOf())
|
|
}
|
|
if !bytes.Equal(id[:8], prefix[:]) {
|
|
t.Fatalf("server prefix: %x", id[:8])
|
|
}
|
|
}
|
|
|
|
func TestExchangeIDRoundTrip(t *testing.T) {
|
|
var verifier [8]byte
|
|
copy(verifier[:], []byte("verif-1-"))
|
|
arg := AppendExchangeIDArgs(nil, verifier, []byte("owner-1"), ExchgIDUseNonPnfs)
|
|
d := xdr.NewDecoder(arg)
|
|
if op, err := d.Uint32(); err != nil || op != OpExchangeID {
|
|
t.Fatalf("op: %d, %v", op, err)
|
|
}
|
|
a, err := DecodeExchangeIDArgs(d)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if a.Verifier != verifier || string(a.OwnerID) != "owner-1" ||
|
|
a.Flags != ExchgIDUseNonPnfs || a.Protect != StateProtectNone {
|
|
t.Fatalf("args: %+v", a)
|
|
}
|
|
|
|
res := AppendExchangeIDRes(nil, 77, 0, ExchgIDUseNonPnfs|ExchgIDConfirmedR, []byte("major"))
|
|
rd := xdr.NewDecoder(res)
|
|
if clientid, err := rd.Uint64(); err != nil || clientid != 77 {
|
|
t.Fatalf("clientid: %d, %v", clientid, err)
|
|
}
|
|
if seq, err := rd.Uint32(); err != nil || seq != 0 {
|
|
t.Fatalf("sequence: %d, %v", seq, err)
|
|
}
|
|
if flags, err := rd.Uint32(); err != nil || flags&ExchgIDConfirmedR == 0 {
|
|
t.Fatalf("flags: %x, %v", flags, err)
|
|
}
|
|
if n, err := resBodyLen(OpExchangeID, ErrOK, res); err != nil || n != len(res) {
|
|
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
|
|
}
|
|
}
|
|
|
|
func TestCreateSessionRoundTrip(t *testing.T) {
|
|
arg := AppendCreateSessionArgs(nil, 0x1234, 3, CreateSessionFlagPersist,
|
|
DefaultForeChannel, DefaultBackChannel, 0xcafe)
|
|
d := xdr.NewDecoder(arg)
|
|
if op, err := d.Uint32(); err != nil || op != OpCreateSession {
|
|
t.Fatalf("op: %d, %v", op, err)
|
|
}
|
|
a, err := DecodeCreateSessionArgs(d)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if a.ClientID != 0x1234 || a.Sequence != 3 || a.Flags != CreateSessionFlagPersist ||
|
|
a.Fore.MaxRequest != DefaultForeChannel.MaxRequest ||
|
|
a.Fore.MaxRequests != DefaultForeChannel.MaxRequests ||
|
|
a.Back.MaxRequest != DefaultBackChannel.MaxRequest ||
|
|
a.CBProgram != 0xcafe {
|
|
t.Fatalf("args: %+v", a)
|
|
}
|
|
|
|
id := MakeSessionID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 0x1234)
|
|
res := AppendCreateSessionRes(nil, id, 3, 0, DefaultForeChannel, DefaultBackChannel)
|
|
if n, err := resBodyLen(OpCreateSession, ErrOK, res); err != nil || n != len(res) {
|
|
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
|
|
}
|
|
}
|
|
|
|
func TestSequenceAndDestroyWire(t *testing.T) {
|
|
id := MakeSessionID([8]byte{9, 9, 9, 9, 9, 9, 9, 9}, 42)
|
|
arg := AppendSequenceArgs(nil, id, 5, 1, 7, true)
|
|
a, err := DecodeSequenceArgs(xdr.NewDecoder(arg[4:]))
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if a.SessionID != id || a.Sequence != 5 || a.Slot != 1 ||
|
|
a.HighestSlot != 7 || !a.CacheThis {
|
|
t.Fatalf("args: %+v", a)
|
|
}
|
|
// The result carries exactly the five words after the session id
|
|
// that RFC 7863 fixes: sequence, slot, highest, target, flags.
|
|
res := AppendSequenceRes(nil, id, 5, 1, 7, 0)
|
|
if len(res) != 36 {
|
|
t.Fatalf("sequence result of %d bytes, want 36", len(res))
|
|
}
|
|
if n, err := resBodyLen(OpSequence, ErrOK, res); err != nil || n != 36 {
|
|
t.Fatalf("sequence measure: %d, %v", n, err)
|
|
}
|
|
flags, err := xdr.NewDecoder(res[32:]).Uint32()
|
|
if err != nil || flags != 0 {
|
|
t.Fatalf("sr_status_flags: %d, %v", flags, err)
|
|
}
|
|
sid, seq, slot, high, target, statusFlags, err := DecodeSequenceRes(xdr.NewDecoder(res))
|
|
if err != nil || sid != id || seq != 5 || slot != 1 || high != 7 ||
|
|
target != 7 || statusFlags != 0 {
|
|
t.Fatalf("decode res: %d %d %d %d %d %d, %v", seq, slot, high, target, statusFlags, 0, err)
|
|
}
|
|
|
|
destroy := AppendDestroySessionArgs(nil, id)
|
|
d := xdr.NewDecoder(destroy)
|
|
if op, err := d.Uint32(); err != nil || op != OpDestroySession {
|
|
t.Fatalf("destroy op: %d, %v", op, err)
|
|
}
|
|
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, id[:]) {
|
|
t.Fatalf("destroy id: %x, %v", raw, err)
|
|
}
|
|
}
|
|
|
|
func TestExchangeIDImplArray(t *testing.T) {
|
|
// One implementation entry walks exactly its three fields: domain,
|
|
// name and the date, the shape RFC 7863 fixes. A trailing dummy
|
|
// word stays in the stream and is the next operation's business.
|
|
var blob []byte
|
|
blob = append(blob, make([]byte, 8)...) // verifier
|
|
blob = xdr.AppendVarOpaque(blob, []byte("id")) // owner
|
|
blob = xdr.AppendUint32(blob, 0) // flags
|
|
blob = xdr.AppendUint32(blob, StateProtectNone)
|
|
blob = xdr.AppendUint32(blob, 1) // one impl entry
|
|
blob = xdr.AppendString(blob, "example.org")
|
|
blob = xdr.AppendString(blob, "probe")
|
|
blob = xdr.AppendUint64(blob, 0) // date seconds
|
|
blob = xdr.AppendUint32(blob, 0) // date nanoseconds
|
|
|
|
d := xdr.NewDecoder(blob)
|
|
a, err := DecodeExchangeIDArgs(d)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if string(a.OwnerID) != "id" || d.Remaining() != 0 {
|
|
t.Fatalf("owner %q, %d bytes left", a.OwnerID, d.Remaining())
|
|
}
|
|
|
|
// The word some clients append behind the entry is not consumed by
|
|
// this decoder; the compound walk sees it as the next argop.
|
|
withExtra := append(append([]byte{}, blob...), 0, 0, 0, 0)
|
|
d = xdr.NewDecoder(withExtra)
|
|
if _, err := DecodeExchangeIDArgs(d); err != nil || d.Remaining() != 4 {
|
|
t.Fatalf("decode with extra: %v, %d bytes left", err, d.Remaining())
|
|
}
|
|
}
|
|
|
|
func TestDecodeSessionChannelIrd(t *testing.T) {
|
|
// A channel with the RDMA sizes present walks them all.
|
|
var body []byte
|
|
body = xdr.AppendUint32(body, 0) // header pad
|
|
body = xdr.AppendUint32(body, 1<<20) // max request
|
|
body = xdr.AppendUint32(body, 1<<20) // max response
|
|
body = xdr.AppendUint32(body, 1<<20) // resourced
|
|
body = xdr.AppendUint32(body, 4) // max operations
|
|
body = xdr.AppendUint32(body, 2) // max requests
|
|
body = xdr.AppendUint32(body, 2) // two rdma ird sizes
|
|
body = xdr.AppendUint32(body, 1)
|
|
body = xdr.AppendUint32(body, 4)
|
|
|
|
c, err := DecodeSessionChannel(xdr.NewDecoder(body))
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !c.RdmaIRDEnabled || len(c.RdmaIRSizes) != 2 || c.RdmaIRSizes[1] != 4 ||
|
|
c.MaxRequests != 2 {
|
|
t.Fatalf("channel: %+v", c)
|
|
}
|
|
}
|
|
|
|
func TestExchangeIDStateProtectReject(t *testing.T) {
|
|
// A state protection other than the plain none is refused with a
|
|
// dedicated error, because this build does not speak RPCSEC_GSS
|
|
// state protection yet.
|
|
var blob []byte
|
|
blob = append(blob, make([]byte, 8)...)
|
|
blob = xdr.AppendVarOpaque(blob, []byte("id"))
|
|
blob = xdr.AppendUint32(blob, 0)
|
|
blob = xdr.AppendUint32(blob, StateProtectMachCred)
|
|
if _, err := DecodeExchangeIDArgs(xdr.NewDecoder(blob)); err == nil {
|
|
t.Fatal("an unsupported state protection decoded")
|
|
}
|
|
}
|
|
|
|
func TestOpenCloseWire(t *testing.T) {
|
|
arg := AppendOpenArgs(nil, 0xfeed, []byte("owner"), ShareAccessBoth, uint32(0),
|
|
true, 0o640, "file.txt")
|
|
d := xdr.NewDecoder(arg)
|
|
if op, err := d.Uint32(); err != nil || op != OpOpen {
|
|
t.Fatalf("op: %d, %v", op, err)
|
|
}
|
|
a, err := DecodeOpenArgs(d)
|
|
if err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if !a.Create || a.Access != ShareAccessBoth || a.Deny != uint32(0) ||
|
|
a.ClientID != 0xfeed || string(a.Owner) != "owner" || a.Perm != 0o640 ||
|
|
a.Name != "file.txt" {
|
|
t.Fatalf("open args: %+v", a)
|
|
}
|
|
|
|
// NOCREATE: the create switch is absent.
|
|
arg = AppendOpenArgs(nil, 1, []byte("o"), ShareAccessRead, 0, false, 0, "there.txt")
|
|
a, err = DecodeOpenArgs(xdr.NewDecoder(arg[4:]))
|
|
if err != nil || a.Create || a.Name != "there.txt" {
|
|
t.Fatalf("nocreate args: %+v, %v", a, err)
|
|
}
|
|
|
|
res := AppendOpenRes(nil, AllZero)
|
|
if n, err := resBodyLen(OpOpen, ErrOK, res); err != nil || n != len(res) {
|
|
t.Fatalf("open measure: %d of %d, %v", n, len(res), err)
|
|
}
|
|
|
|
closeArg := AppendCloseArgs(nil, AllZero)
|
|
d = xdr.NewDecoder(closeArg)
|
|
if op, err := d.Uint32(); err != nil || op != OpClose {
|
|
t.Fatalf("close op: %d, %v", op, err)
|
|
}
|
|
if seqid, err := d.Uint32(); err != nil || seqid != 0 {
|
|
t.Fatalf("close seqid: %d, %v", seqid, err)
|
|
}
|
|
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, AllZero[:]) {
|
|
t.Fatalf("close stateid: %x, %v", raw, err)
|
|
}
|
|
closeRes := AppendCloseRes(nil, AllZero)
|
|
if n, err := resBodyLen(OpClose, ErrOK, closeRes); err != nil || n != 16 {
|
|
t.Fatalf("close measure: %d, %v", n, err)
|
|
}
|
|
}
|
|
|
|
func TestOpenDowngradeDestroyReclaimWire(t *testing.T) {
|
|
// OPEN_DOWNGRADE args: seqid, stateid, access, deny.
|
|
arg := AppendOpenDowngradeArgs(nil, AllZero, ShareAccessRead, 0)
|
|
d := xdr.NewDecoder(arg)
|
|
if op, err := d.Uint32(); err != nil || op != OpOpenDowngrade {
|
|
t.Fatalf("op: %d, %v", op, err)
|
|
}
|
|
if seqid, err := d.Uint32(); err != nil || seqid != 0 {
|
|
t.Fatalf("seqid: %d, %v", seqid, err)
|
|
}
|
|
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, AllZero[:]) {
|
|
t.Fatalf("stateid: %x, %v", raw, err)
|
|
}
|
|
if access, err := d.Uint32(); err != nil || access != ShareAccessRead {
|
|
t.Fatalf("access: %v", err)
|
|
}
|
|
if deny, err := d.Uint32(); err != nil || deny != 0 {
|
|
t.Fatalf("deny: %v", err)
|
|
}
|
|
dgRes := AppendOpenDowngradeRes(nil)
|
|
if n, err := resBodyLen(OpOpenDowngrade, ErrOK, dgRes); err != nil || n != 20 {
|
|
t.Fatalf("downgrade measure: %d, %v", n, err)
|
|
}
|
|
|
|
// DESTROY_CLIENTID args and its empty result body.
|
|
dc := AppendDestroyClientIDArgs(nil, 0x1234)
|
|
d = xdr.NewDecoder(dc)
|
|
if op, err := d.Uint32(); err != nil || op != OpDestroyClientID {
|
|
t.Fatalf("destroy op: %d, %v", op, err)
|
|
}
|
|
if cid, err := d.Uint64(); err != nil || cid != 0x1234 {
|
|
t.Fatalf("clientid: %v", err)
|
|
}
|
|
if n, err := resBodyLen(OpDestroyClientID, ErrOK, nil); err != nil || n != 0 {
|
|
t.Fatalf("destroy measure: %d, %v", n, err)
|
|
}
|
|
|
|
// RECLAIM_COMPLETE args: the one client flag alone, RFC 7863.
|
|
rc := AppendReclaimCompleteArgs(nil, false)
|
|
d = xdr.NewDecoder(rc)
|
|
if op, err := d.Uint32(); err != nil || op != OpReclaimComplete {
|
|
t.Fatalf("op: %d, %v", op, err)
|
|
}
|
|
if one, err := d.Bool(); err != nil || one {
|
|
t.Fatalf("one_client: %v", err)
|
|
}
|
|
if d.Remaining() != 0 {
|
|
t.Fatalf("%d bytes left in the reclaim args", d.Remaining())
|
|
}
|
|
if n, err := resBodyLen(OpReclaimComplete, ErrOK, nil); err != nil || n != 0 {
|
|
t.Fatalf("reclaim measure: %d, %v", n, err)
|
|
}
|
|
}
|
|
|
|
func TestAppendOpenResDelegLength(t *testing.T) {
|
|
// The write delegation carries the space limit union and the
|
|
// permissions ace on top of the read form, RFC 7863: the whole
|
|
// successful OPEN result measures 96 bytes with one.
|
|
var st Stateid
|
|
st[0] = 1
|
|
res := AppendOpenResDeleg(nil, st, OpenDelegWrite, st)
|
|
if len(res) != 96 {
|
|
t.Fatalf("res %d bytes, want 96", len(res))
|
|
}
|
|
// The read form ends after the ace: 84 bytes.
|
|
res = AppendOpenResDeleg(nil, st, OpenDelegRead, st)
|
|
if len(res) != 84 {
|
|
t.Fatalf("read res %d bytes, want 84", len(res))
|
|
}
|
|
if _, _, _, err := DecodeOpenResDeleg(res); err != nil {
|
|
t.Fatalf("read round trip: %v", err)
|
|
}
|
|
}
|