// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4 import ( "bytes" "testing" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // layoutOf decodes the single segment a test layout response carries. func layoutOf(t *testing.T, body []byte) (Layout4, []byte) { t.Helper() d := xdr.NewDecoder(body) var st Stateid raw, err := d.Raw(16) if err != nil { t.Fatalf("layout stateid: %v", err) } copy(st[:], raw) roc, err := d.Bool() if err != nil || roc { t.Fatalf("return on close: %v %v", roc, err) } n, err := d.Uint32() if err != nil || n != 1 { t.Fatalf("layout count %d: %v", n, err) } var l Layout4 if l.Offset, err = d.Uint64(); err != nil { t.Fatal(err) } if l.Length, err = d.Uint64(); err != nil { t.Fatal(err) } if l.IoMode, err = d.Uint32(); err != nil { t.Fatal(err) } if l.Type, err = d.Uint32(); err != nil { t.Fatal(err) } if l.Body, err = d.VarOpaque(); err != nil { t.Fatal(err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the layout response", d.Remaining()) } return l, st[:] } func flexBodyOf(t *testing.T, body []byte) (FlexDataServer, uint64) { t.Helper() d := xdr.NewDecoder(body) stripeUnit, err := d.Uint64() if err != nil { t.Fatal(err) } mirrors, err := d.Uint32() if err != nil || mirrors != 1 { t.Fatalf("mirror count %d: %v", mirrors, err) } servers, err := d.Uint32() if err != nil || servers != 1 { t.Fatalf("data server count %d: %v", servers, err) } var ds FlexDataServer raw, rerr := d.Raw(16) if rerr != nil { t.Fatalf("device id: %v", rerr) } copy(ds.DeviceID[:], raw) if ds.Efficiency, err = d.Uint32(); err != nil { t.Fatal(err) } raw, rerr = d.Raw(16) if rerr != nil { t.Fatalf("data server stateid: %v", rerr) } copy(ds.Stateid[:], raw) fhCount, ferr := d.Uint32() if ferr != nil || fhCount != 1 { t.Fatalf("file handle count %d: %v", fhCount, ferr) } var fhBody []byte if fhBody, err = d.VarOpaque(); err != nil { t.Fatal(err) } ds.FHs = [][]byte{fhBody} if ds.User, err = d.String(); err != nil { t.Fatal(err) } if ds.Group, err = d.String(); err != nil { t.Fatal(err) } if d.Remaining() != 8 { t.Fatalf("%d bytes left in the flexfiles body", d.Remaining()) } return ds, stripeUnit } func TestLayoutGetResBodyLen(t *testing.T) { t.Parallel() st := Stateid{1, 'L', 'A', 'Y', 1, 2, 3} fh := []byte("filehandle") body := AppendFlexFileLayoutBody(nil, 0, 4096, []FlexMirror{{ DataServers: []FlexDataServer{{DeviceID: [16]byte{9}, Stateid: st, FHs: [][]byte{fh}}}, }}) res := AppendLayoutGetRes(nil, st, false, []Layout4{{ Offset: 0, Length: ^uint64(0), IoMode: IoModeRW, Type: LayoutTypeFlexfiles, Body: body, }}) n, err := resBodyLen(OpLayoutGet, ErrOK, res) if err != nil { t.Fatalf("resBodyLen: %v", err) } if n != len(res) { t.Fatalf("measured %d, body is %d", n, len(res)) } l, _ := layoutOf(t, res) ds, stripe := flexBodyOf(t, l.Body) if l.Type != LayoutTypeFlexfiles || l.IoMode != IoModeRW || l.Offset != 0 || l.Length != ^uint64(0) { t.Fatalf("segment %+v", l) } if stripe != 4096 { t.Fatalf("stripe unit %d", stripe) } if ds.DeviceID != [16]byte{9} || len(ds.FHs) != 1 || !bytes.Equal(ds.FHs[0], fh) || ds.Stateid != st { t.Fatalf("data server %+v", ds) } } func TestLayoutCommitReturnDeviceInfoResBodyLen(t *testing.T) { t.Parallel() st := Stateid{2, 'L', 'A', 'Y', 4, 5, 6} commit := AppendLayoutCommitRes(nil, 101) if n, err := resBodyLen(OpLayoutCommit, ErrOK, commit); err != nil || n != len(commit) { t.Fatalf("commit res: %d %v", n, err) } ret := AppendLayoutReturnRes(nil, st) if n, err := resBodyLen(OpLayoutReturn, ErrOK, ret); err != nil || n != len(ret) { t.Fatalf("return res: %d %v", n, err) } addr := AppendFlexDeviceAddr(nil, FlexDeviceAddr{ NetAddrs: []NetAddr{{Netid: "tcp", Uaddr: "127.0.0.1.8.1"}}, Versions: []FlexVersion{{Version: 4, MinorVersion: 2, RSize: 1 << 20, WSize: 1 << 20}}, }) dev := AppendGetDeviceInfoRes(nil, addr) n, err := resBodyLen(OpGetDeviceInfo, ErrOK, dev) if err != nil { t.Fatalf("device info res: %v", err) } if n != len(dev) { t.Fatalf("measured %d, body is %d", n, len(dev)) } d := xdr.NewDecoder(dev) if typ, derr := d.Uint32(); derr != nil || typ != LayoutTypeFlexfiles { t.Fatalf("address type %d: %v", typ, derr) } blob, derr := d.VarOpaque() if derr != nil { t.Fatal(derr) } got, derr := DecodeFlexDeviceAddr(blob) if derr != nil { t.Fatalf("decode device address: %v", derr) } if len(got.NetAddrs) != 1 || got.NetAddrs[0].Netid != "tcp" || got.NetAddrs[0].Uaddr != "127.0.0.1.8.1" { t.Fatalf("net addresses %+v", got.NetAddrs) } if len(got.Versions) != 1 || got.Versions[0] != (FlexVersion{Version: 4, MinorVersion: 2, RSize: 1 << 20, WSize: 1 << 20}) { t.Fatalf("versions %+v", got.Versions) } if _, derr = ReadBitmap(d); derr != nil { t.Fatal(derr) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the device info response", d.Remaining()) } } func TestLayoutArgShapes(t *testing.T) { t.Parallel() st := Stateid{7, 'L', 'A', 'Y', 8, 8, 8} // LAYOUTGET args carry the op code and the full field set. args := AppendLayoutGetArgs(nil, false, LayoutTypeFlexfiles, IoModeRW, 0, 100, 100, st, 1<<16) if got := args[0:4]; !bytes.Equal(got, []byte{0, 0, 0, OpLayoutGet & 0xff}) { t.Fatalf("layout get args open with % x", got) } d := xdr.NewDecoder(args[4:]) if _, err := d.Bool(); err != nil { t.Fatal(err) } if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles { t.Fatalf("type %d: %v", typ, err) } if mode, err := d.Uint32(); err != nil || mode != IoModeRW { t.Fatalf("iomode %d: %v", mode, err) } for range 3 { if _, err := d.Uint64(); err != nil { t.Fatal(err) } } raw, err := d.Raw(16) if err != nil || !bytes.Equal(raw, st[:]) { t.Fatalf("stateid % x: %v", raw, err) } if mc, err := d.Uint32(); err != nil || mc != 1<<16 { t.Fatalf("maxcount %d: %v", mc, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the layout get args", d.Remaining()) } // The file return carries the range, the stateid and the empty ffsid // body; the whole client return carries nothing after the header. fileArgs := AppendLayoutReturnFileArgs(nil, false, LayoutTypeFlexfiles, IoModeRW, 10, 90, st) d = xdr.NewDecoder(fileArgs[4:]) reclaim, err := d.Bool() if err != nil || reclaim { t.Fatalf("reclaim %v: %v", reclaim, err) } if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles { t.Fatalf("type %d: %v", typ, err) } if mode, err := d.Uint32(); err != nil || mode != IoModeRW { t.Fatalf("iomode %d: %v", mode, err) } if kind, err := d.Uint32(); err != nil || kind != ReturnFile { t.Fatalf("return kind %d: %v", kind, err) } if off, err := d.Uint64(); err != nil || off != 10 { t.Fatalf("offset %d: %v", off, err) } if length, err := d.Uint64(); err != nil || length != 90 { t.Fatalf("length %d: %v", length, err) } raw, err = d.Raw(16) if err != nil || !bytes.Equal(raw, st[:]) { t.Fatalf("stateid % x: %v", raw, err) } if ffsid, err := d.Uint32(); err != nil || ffsid != 0 { t.Fatalf("ffsid type %d: %v", ffsid, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the file return", d.Remaining()) } allArgs := AppendLayoutReturnArgs(nil, true, LayoutTypeFlexfiles, IoModeRW, ReturnAll) d = xdr.NewDecoder(allArgs[4:]) if reclaim, err := d.Bool(); err != nil || !reclaim { t.Fatalf("reclaim %v: %v", reclaim, err) } for range 3 { if _, err := d.Uint32(); err != nil { t.Fatal(err) } } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the whole client return", d.Remaining()) } } func TestLayoutCommitArgsShape(t *testing.T) { t.Parallel() st := Stateid{3, 'L', 'A', 'Y', 9, 9, 9} full := LayoutCommitArgs{ Offset: 8, Length: 92, Reclaim: true, LayoutSt: st, LastWriteSet: true, LastWrite: 99, TimeSet: true, Seconds: 1758000000, Nseconds: 500, UpdateType: 1, UpdateBody: []byte("ff"), }.Encode() d := xdr.NewDecoder(full[4:]) if off, err := d.Uint64(); err != nil || off != 8 { t.Fatalf("offset %d: %v", off, err) } if length, err := d.Uint64(); err != nil || length != 92 { t.Fatalf("length %d: %v", length, err) } if reclaim, err := d.Bool(); err != nil || !reclaim { t.Fatalf("reclaim %v: %v", reclaim, err) } raw, err := d.Raw(16) if err != nil || !bytes.Equal(raw, st[:]) { t.Fatalf("stateid % x: %v", raw, err) } if set, err := d.Bool(); err != nil || !set { t.Fatalf("last write set %v: %v", set, err) } if lw, err := d.Uint64(); err != nil || lw != 99 { t.Fatalf("last write %d: %v", lw, err) } if ts, err := d.Bool(); err != nil || !ts { t.Fatalf("time set %v: %v", ts, err) } if sec, err := d.Int64(); err != nil || sec != 1758000000 { t.Fatalf("seconds %d: %v", sec, err) } if ns, err := d.Uint32(); err != nil || ns != 500 { t.Fatalf("nseconds %d: %v", ns, err) } if ut, err := d.Uint32(); err != nil || ut != 1 { t.Fatalf("update type %d: %v", ut, err) } if body, err := d.VarOpaque(); err != nil || !bytes.Equal(body, []byte("ff")) { t.Fatalf("update body %q: %v", body, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the commit args", d.Remaining()) } // The empty form: no last write, no time, no update body. empty := LayoutCommitArgs{LayoutSt: st}.Encode() d = xdr.NewDecoder(empty[4:]) for range 2 { if _, err := d.Uint64(); err != nil { t.Fatal(err) } } if _, err := d.Bool(); err != nil { t.Fatal(err) } if _, err := d.Raw(16); err != nil { t.Fatal(err) } if set, err := d.Bool(); err != nil || set { t.Fatalf("empty last write %v: %v", set, err) } if ts, err := d.Bool(); err != nil || ts { t.Fatalf("empty time %v: %v", ts, err) } if _, err := d.Uint32(); err != nil { t.Fatal(err) } if body, err := d.VarOpaque(); err != nil || len(body) != 0 { t.Fatalf("empty update body %q: %v", body, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the empty commit args", d.Remaining()) } } func TestGetDeviceInfoArgsShape(t *testing.T) { t.Parallel() var device [16]byte copy(device[:], "0123456789abcdef") args := AppendGetDeviceInfoArgs(nil, device, LayoutTypeFlexfiles, 1<<16, Bitmap{}.With(0)) d := xdr.NewDecoder(args[4:]) raw, err := d.Raw(16) if err != nil || !bytes.Equal(raw, device[:]) { t.Fatalf("device id % x: %v", raw, err) } if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles { t.Fatalf("type %d: %v", typ, err) } if mc, err := d.Uint32(); err != nil || mc != 1<<16 { t.Fatalf("maxcount %d: %v", mc, err) } if _, err := ReadBitmap(d); err != nil { t.Fatal(err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the device info args", d.Remaining()) } } func TestOps41Shapes(t *testing.T) { t.Parallel() // VERIFY args are a bare fattr4 with the op in front. args := AppendVerifyArgs(nil, OfBits(AttrMode), Attrs{Mode: 0o644}) d := xdr.NewDecoder(args[4:]) if _, err := ReadBitmap(d); err != nil { t.Fatal(err) } if _, err := d.VarOpaque(); err != nil { t.Fatal(err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the verify args", d.Remaining()) } // BIND_CONN_TO_SESSION echoes the session and the direction; its // result body measures to exactly 20 bytes. sid := SessionID{1, 2, 3} res := AppendBindConnToSessionRes(nil, sid, Cdfs4Both) if n, err := resBodyLen(OpBindConnToSession, ErrOK, res); err != nil || n != len(res) || n != 20 { t.Fatalf("bind res measured %d of %d: %v", n, len(res), err) } d = xdr.NewDecoder(res) raw, err := d.Raw(16) if err != nil || !bytes.Equal(raw, sid[:]) { t.Fatalf("session % x: %v", raw, err) } if dir, err := d.Uint32(); err != nil || dir != Cdfs4Both { t.Fatalf("dir %d: %v", dir, err) } // TEST_STATEID measures to the status array. statuses := AppendTestStateidRes(nil, []uint32{ErrOK, ErrBadStateid}) if n, err := resBodyLen(OpTestStateid, ErrOK, statuses); err != nil || n != len(statuses) { t.Fatalf("test stateid res measured %d of %d: %v", n, len(statuses), err) } // RELEASE_LOCKOWNER and DELEGRETURN carry their shapes. own := AppendReleaseLockOwnerArgs(nil, 0x99, []byte("owner")) d = xdr.NewDecoder(own[4:]) if cid, err := d.Uint64(); err != nil || cid != 0x99 { t.Fatalf("clientid %d: %v", cid, err) } if o, err := d.VarOpaque(); err != nil || string(o) != "owner" { t.Fatalf("owner %q: %v", o, err) } st := Stateid{1, 'D', 'E', 'L', 7} dr := AppendDelegReturnArgs(nil, st) d = xdr.NewDecoder(dr[4:]) raw, err = d.Raw(16) if err != nil || !bytes.Equal(raw, st[:]) { t.Fatalf("deleg return stateid % x: %v", raw, err) } fs := AppendFreeStateidArgs(nil, st) d = xdr.NewDecoder(fs[4:]) raw, err = d.Raw(16) if err != nil || !bytes.Equal(raw, st[:]) { t.Fatalf("free stateid % x: %v", raw, err) } bc := AppendBackchannelCtlArgs(nil, 0x40000000) d = xdr.NewDecoder(bc[4:]) if p, err := d.Uint32(); err != nil || p != 0x40000000 { t.Fatalf("program %d: %v", p, err) } ts := AppendTestStateidArgs(nil, []Stateid{st, {}}) d = xdr.NewDecoder(ts[4:]) if n, err := d.Uint32(); err != nil || n != 2 { t.Fatalf("count %d: %v", n, err) } for range 2 { raw, err = d.Raw(16) if err != nil { t.Fatal(err) } } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the test stateid args", d.Remaining()) } } func TestXattrShapes(t *testing.T) { t.Parallel() // GETXATTR args carry the name alone, RFC 8276: the operation // number, the length word and the twelve byte name. args := AppendGetXattrArgs(nil, "user.comment") if len(args) != 20 { t.Fatalf("getxattr args of %d bytes, want 20", len(args)) } d := xdr.NewDecoder(args[4:]) if name, err := d.String(); err != nil || name != "user.comment" { t.Fatalf("name %q: %v", name, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the getxattr args", d.Remaining()) } res := AppendGetXattrRes(nil, []byte("hello")) n, err := resBodyLen(OpGetxattr, ErrOK, res) if err != nil || n != len(res) { t.Fatalf("getxattr res measured %d of %d: %v", n, len(res), err) } if v, err := DecodeGetXattrRes(res); err != nil || string(v) != "hello" { t.Fatalf("getxattr value %q: %v", v, err) } // LISTXATTR res: the cookie, the names and the eof flag. list := AppendListXattrRes(nil, 7, []string{"user.a", "user.b"}, false) n, err = resBodyLen(OpListxattr, ErrOK, list) if err != nil || n != len(list) { t.Fatalf("listxattr res measured %d of %d: %v", n, len(list), err) } // SETXATTR args: the mode first, then the key and the value. sx := AppendSetXattrArgs(nil, SetXattrCreate, "user.a", []byte("v")) d = xdr.NewDecoder(sx[4:]) if mode, err := d.Uint32(); err != nil || mode != SetXattrCreate { t.Fatalf("mode %d: %v", mode, err) } if name, err := d.String(); err != nil || name != "user.a" { t.Fatalf("set name %q: %v", name, err) } if v, err := d.VarOpaque(); err != nil || string(v) != "v" { t.Fatalf("value %q: %v", v, err) } rx := AppendRemoveXattrArgs(nil, "user.a") d = xdr.NewDecoder(rx[4:]) if name, err := d.String(); err != nil || name != "user.a" { t.Fatalf("remove name %q: %v", name, err) } } func TestOps41RemainingShapes(t *testing.T) { t.Parallel() // The argument-less ops carry exactly the op code. if got := AppendLookupp(nil); !bytes.Equal(got, []byte{0, 0, 0, OpLookupp & 0xff}) { t.Fatalf("lookupp % x", got) } if got := AppendPutPubfh(nil); !bytes.Equal(got, []byte{0, 0, 0, OpPutPubfh & 0xff}) { t.Fatalf("putpubfh % x", got) } // NVERIFY shares the VERIFY shape. nv := AppendNverifyArgs(nil, OfBits(AttrSize), Attrs{Size: 9}) d := xdr.NewDecoder(nv[4:]) if _, err := ReadBitmap(d); err != nil { t.Fatal(err) } if _, err := d.VarOpaque(); err != nil { t.Fatal(err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left in the nverify args", d.Remaining()) } // BIND_CONN_TO_SESSION args: the session, the direction and the // RDMA mode flag. bind := AppendBindConnToSessionArgs(nil, SessionID{9}, Cdfc4Back, true) d = xdr.NewDecoder(bind[4:]) raw, err := d.Raw(16) if err != nil || raw[0] != 9 { t.Fatalf("session % x: %v", raw, err) } if dir, err := d.Uint32(); err != nil || dir != Cdfc4Back { t.Fatalf("dir %d: %v", dir, err) } if rdma, err := d.Bool(); err != nil || !rdma { t.Fatalf("rdma %v: %v", rdma, err) } // LISTXATTR args carry the cookie and the budget. la := AppendListXattrArgs(nil, 3, 4096) d = xdr.NewDecoder(la[4:]) if ck, err := d.Uint64(); err != nil || ck != 3 { t.Fatalf("list cookie %d: %v", ck, err) } if mc, err := d.Uint32(); err != nil || mc != 4096 { t.Fatalf("list maxcount %d: %v", mc, err) } }