// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package rpc import ( "bytes" "errors" "testing" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) func TestCallRoundTrip(t *testing.T) { cred := Auth{Flavor: FlavorSys, Body: []byte{1, 2, 3}} c := Call{XID: 0xfeedface, Program: 100003, Version: 4, Procedure: 1, Cred: cred} rec, err := AppendCall(nil, c) if err != nil { t.Fatalf("encode: %v", err) } rec = append(rec, xdr.AppendString(nil, "tag")...) got, args, err := DecodeCall(rec) if err != nil { t.Fatalf("decode: %v", err) } if got.XID != c.XID || got.Program != c.Program || got.Version != c.Version || got.Procedure != c.Procedure || got.Cred.Flavor != c.Cred.Flavor || !bytes.Equal(got.Cred.Body, c.Cred.Body) { t.Fatalf("round trip: got %+v, want %+v", got, c) } tag, err := xdr.NewDecoder(args).String() if err != nil || tag != "tag" { t.Fatalf("arguments after the header: %q, %v", tag, err) } } func TestDecodeCallRejectsReplies(t *testing.T) { rec := xdr.AppendUint32(xdr.AppendUint32(nil, 1), MsgReply) if _, _, err := DecodeCall(rec); err == nil { t.Fatal("a reply record decoded as a call") } } func TestOversizedAuthBody(t *testing.T) { c := Call{XID: 1, Program: 100003, Version: Version, Procedure: 0, Cred: Auth{Flavor: FlavorSys, Body: make([]byte, maxAuthBody+1)}} if _, err := AppendCall(nil, c); err == nil { t.Fatal("an oversized credential encoded without error") } } func TestAcceptedReplyRoundTrip(t *testing.T) { rec, err := AppendAcceptedReply(nil, 7, AuthNull, AcceptSuccess, Mismatch{}) if err != nil { t.Fatalf("encode: %v", err) } rec = append(rec, xdr.AppendUint32(nil, 42)...) r, err := DecodeReply(rec) if err != nil { t.Fatalf("decode: %v", err) } if r.XID != 7 || r.Status != AcceptSuccess { t.Fatalf("got xid %d status %d", r.XID, r.Status) } body, err := xdr.NewDecoder(r.Body).Uint32() if err != nil || body != 42 { t.Fatalf("result body: %d, %v", body, err) } } func TestProgMismatchReply(t *testing.T) { rec, err := AppendAcceptedReply(nil, 9, AuthNull, AcceptProgMismatch, Mismatch{Low: 4, High: 4}) if err != nil { t.Fatalf("encode: %v", err) } r, err := DecodeReply(rec) if err != nil { t.Fatalf("decode: %v", err) } if r.Status != AcceptProgMismatch || r.Mismatch != (Mismatch{4, 4}) { t.Fatalf("got status %d mismatch %+v", r.Status, r.Mismatch) } } func TestUnknownAcceptStatus(t *testing.T) { if _, err := AppendAcceptedReply(nil, 1, AuthNull, 99, Mismatch{}); err == nil { t.Fatal("an unknown accept status encoded without error") } } func TestAuthSysRoundTrip(t *testing.T) { a := AuthSys{Stamp: 12, Machine: "client", UID: 1000, GID: 100, GIDs: []uint32{100, 5, 27}} body, err := a.Body() if err != nil { t.Fatalf("encode: %v", err) } got, err := DecodeAuthSysBody(body) if err != nil { t.Fatalf("decode: %v", err) } if got.Stamp != a.Stamp || got.Machine != a.Machine || got.UID != a.UID || got.GID != a.GID || !equalU32(got.GIDs, a.GIDs) { t.Fatalf("round trip: got %+v, want %+v", got, a) } } func equalU32(a, b []uint32) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func TestAuthSysTooManyGroups(t *testing.T) { a := AuthSys{GIDs: make([]uint32, 17)} if _, err := a.Body(); err == nil { t.Fatal("17 supplementary groups encoded without error") } if _, err := DecodeAuthSysBody(xdr.AppendUint32(xdr.AppendUint32(nil, 0), 17)); err == nil { t.Fatal("a credential claiming 17 groups decoded without error") } } func TestDecodeRejectedReply(t *testing.T) { // RPC_MISMATCH carries the version range. rec := xdr.AppendUint32(nil, 5) rec = xdr.AppendUint32(rec, MsgReply) rec = xdr.AppendUint32(rec, 1) // rejected rec = xdr.AppendUint32(rec, RejectRPCMismatch) rec = xdr.AppendUint32(rec, 2) rec = xdr.AppendUint32(rec, 4) r, err := DecodeReply(rec) if err != nil || r.XID != 5 || !r.Rejected || r.Mismatch != (Mismatch{2, 4}) { t.Fatalf("rpc mismatch: %+v, %v", r, err) } if r.Status != 0 || r.Body != nil { t.Fatalf("a rejected reply carries no result: %+v", r) } // An auth error carries one status word. rec = xdr.AppendUint32(nil, 6) rec = xdr.AppendUint32(rec, MsgReply) rec = xdr.AppendUint32(rec, 1) rec = xdr.AppendUint32(rec, RejectAuthError) rec = xdr.AppendUint32(rec, AuthBadVerf) if r, err = DecodeReply(rec); err != nil || r.XID != 6 || !r.Rejected || r.AuthStat != AuthBadVerf { t.Fatalf("auth error: %+v, %v", r, err) } } func TestDecodeAuthOversizeBody(t *testing.T) { // A credential body beyond 400 bytes is refused, not buffered. huge := xdr.AppendUint32(nil, FlavorSys) huge = xdr.AppendUint32(huge, maxAuthBody+1) huge = append(huge, make([]byte, 8)...) rec := xdr.AppendUint32(nil, 1) rec = xdr.AppendUint32(rec, MsgCall) rec = xdr.AppendUint32(rec, Version) rec = xdr.AppendUint32(rec, 100003) rec = xdr.AppendUint32(rec, 4) rec = xdr.AppendUint32(rec, 0) rec = append(rec, huge...) if _, _, err := DecodeCall(rec); !errors.Is(err, ErrBadMessage) { t.Fatalf("an oversized credential decoded as %v", err) } } func TestDecodeReplyGarbage(t *testing.T) { for _, rec := range [][]byte{ nil, {0, 0, 0}, // short xdr.AppendUint32(nil, 1), // no type {0, 0, 0, 1, 0, 0, 0, 9}, // unknown reply stat 9 } { if _, err := DecodeReply(rec); !errors.Is(err, ErrBadMessage) { t.Fatalf("record %x decoded with %v, want ErrBadMessage", rec, err) } } } func TestPeekHeader(t *testing.T) { // A minimal call record: xid and the call type. b := xdr.AppendUint32(xdr.AppendUint32(nil, 0x55), MsgCall) xid, mtype, err := PeekHeader(b) if err != nil || xid != 0x55 || mtype != MsgCall { t.Fatalf("call peek: %x %d, %v", xid, mtype, err) } if _, _, err := PeekHeader(nil); !errors.Is(err, ErrBadMessage) { t.Fatalf("an empty record: %v", err) } }