// 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 TestCBCompoundRoundTrip(t *testing.T) { ops := [][]byte{ AppendCBSequenceArgs(nil, MakeSessionID([8]byte{}, 5), 1, 0, 0, true), AppendCBRecallArgs(nil, AllZero, false, []byte{9, 9}), } arg := AppendCBCompoundArgs(nil, "cb", 2, 0, ops) h, d, err := DecodeCBCompoundArgs(arg) if err != nil { t.Fatalf("decode: %v", err) } if h.Tag != "cb" || h.Minor != 2 || h.CallbackIdent != 0 || h.OpCount != 2 { t.Fatalf("header: %+v", h) } if op, err := d.Uint32(); err != nil || op != OpCBSequence { t.Fatalf("op 0: %d, %v", op, err) } a, err := DecodeCBSequenceArgs(d) if err != nil { t.Fatalf("cb sequence: %v", err) } if a.Sequence != 1 || a.Slot != 0 || !a.CacheThis { t.Fatalf("cb sequence args: %+v", a) } if op, err := d.Uint32(); err != nil || op != OpCBRecall { t.Fatalf("op 1: %d, %v", op, err) } r, err := DecodeCBRecallArgs(d) if err != nil || r.Truncate || !bytes.Equal(r.FH, []byte{9, 9}) { t.Fatalf("cb recall args: %+v, %v", r, err) } } func TestCBSequenceResShape(t *testing.T) { id := MakeSessionID([8]byte{}, 0x55) res := AppendCBSequenceRes(nil, id, 9, 2, 7) if len(res) != 32 { t.Fatalf("res of %d bytes, want 32", len(res)) } d := xdr.NewDecoder(res) if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, id[:]) { t.Fatalf("session id: %x, %v", raw, err) } for _, want := range []uint32{9, 2, 7, 7} { if v, err := d.Uint32(); err != nil || v != want { t.Fatalf("word: %d, %v; want %d", v, err, want) } } sid, seq, slot, high, target, err := DecodeCBSequenceRes(xdr.NewDecoder(res)) if err != nil || sid != id || seq != 9 || slot != 2 || high != 7 || target != 7 { t.Fatalf("decode: %d %d %d %d %d, %v", seq, slot, high, target, 0, err) } } func TestCBGetattrArgs(t *testing.T) { arg := AppendCBGetattrArgs(nil, []byte("fh"), OfBits(AttrSize, AttrChange)) d := xdr.NewDecoder(arg) if op, err := d.Uint32(); err != nil || op != OpCBGetattr { t.Fatalf("op: %d, %v", op, err) } if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh" { t.Fatalf("fh: %q, %v", fh, err) } if m, err := ReadBitmap(d); err != nil || !m.Has(AttrSize) || !m.Has(AttrChange) { t.Fatalf("bitmap: %v", m.Words()) } } func TestDecodeCompoundResBodiesCB(t *testing.T) { var results [][]byte seqBody := append(AppendOpHeader(nil, OpCBSequence, ErrOK), AppendCBSequenceRes(nil, MakeSessionID([8]byte{}, 3), 4, 0, 7)...) results = append(results, seqBody, AppendOpHeader(nil, OpCBRecall, ErrOK)) payload := AppendCBCompoundRes(nil, ErrOK, "cbt", results) res, bodies, err := DecodeCompoundResBodiesCB(payload) if err != nil { t.Fatalf("decode: %v", err) } if res.Status != ErrOK || len(res.Ops) != 2 { t.Fatalf("res: status %d ops %d", res.Status, len(res.Ops)) } if len(bodies[0]) != 32 || len(bodies[1]) != 0 { t.Fatalf("bodies: %d and %d", len(bodies[0]), len(bodies[1])) } // A failed op carries no body. payload = AppendCBCompoundRes(nil, ErrNotSupp, "", [][]byte{ AppendOpHeader(nil, OpCBGetattr, ErrNotSupp), }) res, bodies, err = DecodeCompoundResBodiesCB(payload) if err != nil || res.Status != ErrNotSupp || len(bodies[0]) != 0 { t.Fatalf("failed op: status %d body %d, %v", res.Status, len(bodies[0]), err) } } func TestDelegationCodecRoundTrip(t *testing.T) { var st Stateid st[0] = 3 st[4], st[5], st[6], st[7] = 'D', 'E', 'L', 'E' // READ delegation round trip. The res body ends with the NONE type, // which the READ delegation replaces. base := AppendOpenRes(nil, AllZero) body := AppendDelegation(base[:len(base)-4], OpenDelegRead, st, false, 0) openSt, delegType, delegSt, err := DecodeOpenResDeleg(body) if err != nil { t.Fatalf("read decode: %v", err) } if delegType != OpenDelegRead || delegSt != st || openSt != AllZero { t.Fatalf("read delegation: %+v", delegSt) } // WRITE delegation round trip with a space limit. body = AppendDelegation(base[:len(base)-4], OpenDelegWrite, st, true, 1<<20) openSt, delegType, delegSt, err = DecodeOpenResDeleg(body) if err != nil { t.Fatalf("write decode: %v", err) } if delegType != OpenDelegWrite || delegSt != st { t.Fatalf("write delegation: %d, %v", delegType, err) } // NONE carries nothing beyond the type. body = AppendDelegation(base[:len(base)-4], OpenDelegNone, st, false, 0) _, delegType, _, derr := DecodeOpenResDeleg(body) if derr != nil || delegType != OpenDelegNone { t.Fatalf("none: %d, %v", delegType, derr) } if n, err := resBodyLenCB(OpCBSequence, ErrOK, append(AppendOpHeader(nil, OpCBSequence, ErrOK), AppendCBSequenceRes(nil, MakeSessionID([8]byte{}, 1), 1, 0, 7)...)); err != nil || n != 32 { t.Fatalf("cb seq measure: %d, %v", n, err) } } func seqBodyHelper() []byte { return append(AppendOpHeader(nil, OpCBSequence, ErrOK), AppendCBSequenceRes(nil, MakeSessionID([8]byte{}, 1), 1, 0, 7)...) } func TestAppendOpenArgsPrevious(t *testing.T) { arg := AppendOpenArgsPrevious(nil, 0x7777, []byte("owner"), ShareAccessBoth, 0) d := xdr.NewDecoder(arg) if op, err := d.Uint32(); err != nil || op != OpOpen { t.Fatalf("op: %d, %v", op, err) } if seq, err := d.Uint32(); err != nil || seq != 0 { t.Fatalf("seqid: %v", err) } if access, err := d.Uint32(); err != nil || access != ShareAccessBoth { t.Fatalf("access: %v", err) } if deny, err := d.Uint32(); err != nil || deny != 0 { t.Fatalf("deny: %v", err) } if cid, err := d.Uint64(); err != nil || cid != 0x7777 { t.Fatalf("clientid: %v", err) } owner, err := d.VarOpaque() if err != nil || string(owner) != "owner" { t.Fatalf("owner: %q, %v", owner, err) } if ot, err := d.Uint32(); err != nil || ot != 0 { t.Fatalf("opentype: %d, %v", ot, err) } if claim, err := d.Uint32(); err != nil || claim != ClaimPrevious { t.Fatalf("claim: %d, %v", claim, err) } } func TestAppendOpenArgsPreviousRoundTrip(t *testing.T) { arg := AppendOpenArgsPrevious(nil, 0x7777, []byte("owner"), ShareAccessBoth, 0) d := xdr.NewDecoder(arg) if op, err := d.Uint32(); err != nil || op != OpOpen { t.Fatalf("op: %d, %v", op, err) } if seq, err := d.Uint32(); err != nil || seq != 0 { t.Fatalf("seqid: %v", err) } if access, err := d.Uint32(); err != nil || access != ShareAccessBoth { t.Fatalf("access: %v", err) } if deny, err := d.Uint32(); err != nil || deny != 0 { t.Fatalf("deny: %v", err) } if cid, err := d.Uint64(); err != nil || cid != 0x7777 { t.Fatalf("clientid: %v", err) } owner, err := d.VarOpaque() if err != nil || string(owner) != "owner" { t.Fatalf("owner: %q, %v", owner, err) } if ot, err := d.Uint32(); err != nil || ot != 0 { t.Fatalf("opentype: %d, %v", ot, err) } if claim, err := d.Uint32(); err != nil || claim != ClaimPrevious { t.Fatalf("claim: %d, %v", claim, err) } } func TestDecodeOpenResDelegTruncated(t *testing.T) { // Every proper prefix of a READ delegation answer must fail to decode. st := AllZero st[0] = 3 base := AppendOpenRes(nil, AllZero) base = base[:len(base)-4] // drop the NONE delegation type full := append(append([]byte{}, base...), AppendDelegation(nil, OpenDelegRead, st, false, 0)...) for n := 4; n < len(full); n++ { if _, _, _, err := DecodeOpenResDeleg(full[:n]); err == nil { t.Fatalf("a %d byte prefix decoded", n) } } } func TestDecodeSessionChannelRoundTrip(t *testing.T) { // No RDMA: six words and an empty size array. var body []byte body = xdr.AppendUint32(body, 0) body = xdr.AppendUint32(body, 1<<20) body = xdr.AppendUint32(body, 1<<20) body = xdr.AppendUint32(body, 1<<20) body = xdr.AppendUint32(body, 8) body = xdr.AppendUint32(body, 8) body = xdr.AppendUint32(body, 0) c, err := DecodeSessionChannel(xdr.NewDecoder(body)) if err != nil { t.Fatalf("decode: %v", err) } if c.MaxRequests != 8 || c.RdmaIRDEnabled { t.Fatalf("channel: %+v", c) } } func TestDecodeExchangeIDArgsTruncated(t *testing.T) { // AppendLockArgsNew writes the op, the open seqid, the open stateid, // the lock seqid, the client id and the owner; the truncation test // walks the payload after the op number. full := AppendExchangeIDArgs(nil, [8]byte{1}, []byte("own"), 0) for n := 1; n < len(full)-4; n++ { if _, err := DecodeExchangeIDArgs(xdr.NewDecoder(full[4 : 4+n])); err == nil { t.Fatalf("a %d byte prefix decoded", n) } } }