// Copyright (c) 2026 Petr BalvĂ­n (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) } }