// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4server import ( "bytes" "testing" "sourcedock.dev/petrbalvin/nfs/internal/nfs4" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // GETDEVICELIST answers the one device of this build for any layout // type; LAYOUTGET accepts the emulated families and returns their wire // bodies. func TestEmulatedLayoutFamilies(t *testing.T) { h := testTree(t) sid, seq := newSession(t, h) run := func(s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) { t.Helper() all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...) payload := nfs4.AppendCompoundArgs(nil, "fam", nfs4.MinorVersion, all) body, ok := h.compound(payload, cred{uid: 0, gid: 0}) if !ok { t.Fatal("garbage") } r, bodies, derr := nfs4.DecodeCompoundResBodies(body) if derr != nil { t.Fatalf("decode: %v", derr) } return r, bodies[1:] } seq++ res, bodies := run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendGetattr(nil, nfs4.OfBits(nfs4.AttrType)), nfs4.AppendGetDeviceListArgs(nil, nfs4.LayoutTypeFlexfiles, 8, 0, [8]byte{}), }) wantStatus(t, "open", res.Status, nfs4.ErrOK) seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendGetDeviceListArgs(nil, nfs4.LayoutTypeFlexfiles, 8, 0, [8]byte{}), }) wantStatus(t, "device list", res.Status, nfs4.ErrOK) gd := xdr.NewDecoder(bodies[0]) if cookie, cerr := gd.Uint64(); cerr != nil || cookie != 0 { t.Fatalf("cookie %d", cookie) } if _, cerr := gd.Raw(8); cerr != nil { // verifier t.Fatal(cerr) } n, cerr := gd.Uint32() if cerr != nil || n != 1 { t.Fatalf("devices %d: %v", n, cerr) } raw, _ := gd.Raw(16) if !bytes.Equal(raw, layoutDeviceID[:]) { t.Fatalf("device % x", raw) } if eof, _ := gd.Bool(); !eof { t.Fatal("eof false") } // The files layout body decodes with its device and fh list. openSt := nfs4.Stateid{} seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendOpenArgs(nil, 0x8888, []byte("fam"), nfs4.ShareAccessRead, 0, false, 0, "a.txt"), nfs4.AppendGetfh(nil), }) wantStatus(t, "open", res.Status, nfs4.ErrOK) copy(openSt[:], bodies[1]) fh, _ := xdr.NewDecoder(bodies[2]).VarOpaque() seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutfh(nil, fh), nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFiles, nfs4.IoModeRead, 0, ^uint64(0), 0, openSt, 1<<16), }) wantStatus(t, "files layoutget", res.Status, nfs4.ErrOK) ld := xdr.NewDecoder(bodies[1]) var st nfs4.Stateid raw, _ = ld.Raw(16) copy(st[:], raw) if !bytes.HasPrefix(st[4:], []byte("LAYOUT")) { t.Fatalf("stateid % x", st[4:]) } if _, _ = ld.Bool(); ld.Remaining() == 0 { t.Fatal("no layout segments") } if n, _ := ld.Uint32(); n != 1 { // the segment count t.Fatalf("segment count %d", n) } if _, _ = ld.Uint64(); ld.Remaining() == 0 { // offset t.Fatal("short segment") } if _, _ = ld.Uint64(); ld.Remaining() == 0 { // length t.Fatal("short segment") } if _, _ = ld.Uint32(); ld.Remaining() == 0 { // iomode t.Fatal("short segment") } if _, _ = ld.Uint32(); ld.Remaining() == 0 { // type t.Fatal("short segment") } flex, ferr := ld.VarOpaque() if ferr != nil { t.Fatal(ferr) } // The files layout body: device, util, first stripe, pattern offset, // the fh list with the file's own handle. fd := xdr.NewDecoder(flex) dev, _ := fd.Raw(16) if !bytes.Equal(dev, layoutDeviceID[:]) { t.Fatalf("body device % x len %d", dev, len(flex)) } if _, _ = fd.Uint32(); fd.Remaining() == 0 { // util t.Fatal("short files body") } if _, _ = fd.Uint32(); fd.Remaining() == 0 { // first stripe t.Fatal("short files body") } if _, _ = fd.Uint64(); fd.Remaining() == 0 { // pattern offset t.Fatal("short files body") } fn, _ := fd.Uint32() if fn != 1 { t.Fatalf("fh list %d", fn) } if got, _ := fd.VarOpaque(); !bytes.Equal(got, fh) { t.Fatalf("fh % x", got) } // The block, objects and SCSI families answer the same way with // their own bodies; the emulated device is always this server. for _, typ := range []uint32{nfs4.LayoutTypeFlexFilesV2, nfs4.LayoutTypeBlock, nfs4.LayoutTypeObjects, nfs4.LayoutTypeScsi} { seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutfh(nil, fh), nfs4.AppendLayoutGetArgs(nil, false, typ, nfs4.IoModeRead, 0, ^uint64(0), 0, openSt, 1<<16), }) if res.Status != nfs4.ErrOK { t.Fatalf("family %d: status %d", typ, res.Status) } } } // LAYOUTGET of the flexfiles version 2 type answers the version two // body: the data server with its stateid array, the file handle and the // AUTH_NONE credential of tight coupling. func TestFlexFilesV2LayoutGet(t *testing.T) { h := testTree(t) sid, seq := newSession(t, h) run := func(s uint32, ops [][]byte) (nfs4.CompoundRes, [][]byte) { t.Helper() all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, s, 0, defaultSlots-1, true)}, ops...) payload := nfs4.AppendCompoundArgs(nil, "ffv2", nfs4.MinorVersion, all) body, ok := h.compound(payload, cred{uid: 0, gid: 0}) if !ok { t.Fatal("garbage") } r, bodies, derr := nfs4.DecodeCompoundResBodies(body) if derr != nil { t.Fatalf("decode: %v", derr) } return r, bodies[1:] } seq++ res, bodies := run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendOpenArgs(nil, 0x9999, []byte("ffv2"), nfs4.ShareAccessRead, 0, false, 0, "a.txt"), nfs4.AppendGetfh(nil), }) wantStatus(t, "open", res.Status, nfs4.ErrOK) var openSt nfs4.Stateid copy(openSt[:], bodies[1]) fh, _ := xdr.NewDecoder(bodies[2]).VarOpaque() seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutfh(nil, fh), nfs4.AppendLayoutGetArgs(nil, false, nfs4.LayoutTypeFlexFilesV2, nfs4.IoModeRead, 0, ^uint64(0), 0, openSt, 1<<16), }) wantStatus(t, "ffv2 layoutget", res.Status, nfs4.ErrOK) ld := xdr.NewDecoder(bodies[1]) var st nfs4.Stateid raw, _ := ld.Raw(16) copy(st[:], raw) if !bytes.HasPrefix(st[4:], []byte("LAYOUT")) { t.Fatalf("stateid % x", st[4:]) } if _, _ = ld.Bool(); ld.Remaining() == 0 { t.Fatal("no segments") } if n, _ := ld.Uint32(); n != 1 { t.Fatalf("segments %d", n) } if _, _ = ld.Uint64(); ld.Remaining() == 0 { // offset t.Fatal("short segment") } if _, _ = ld.Uint64(); ld.Remaining() == 0 { // length t.Fatal("short segment") } if _, _ = ld.Uint32(); ld.Remaining() == 0 { // iomode t.Fatal("short segment") } if typ, _ := ld.Uint32(); typ != nfs4.LayoutTypeFlexFilesV2 { t.Fatalf("segment type %d", typ) } v2, verr := ld.VarOpaque() if verr != nil { t.Fatal(verr) } fd := xdr.NewDecoder(v2) if su, err := fd.Uint64(); err != nil || su != 0 { t.Fatalf("stripe unit %d: %v", su, err) } if n, err := fd.Uint32(); err != nil || n != 1 { t.Fatalf("mirrors %d: %v", n, err) } if n, err := fd.Uint32(); err != nil || n != 1 { t.Fatalf("servers %d: %v", n, err) } raw, _ = fd.Raw(16) if !bytes.Equal(raw, layoutDeviceID[:]) { t.Fatalf("device % x", raw) } if eff, err := fd.Uint32(); err != nil || eff != 0 { t.Fatalf("efficiency %d: %v", eff, err) } if n, err := fd.Uint32(); err != nil || n != 1 { t.Fatalf("stateids %d: %v", n, err) } raw, _ = fd.Raw(16) if !bytes.Equal(raw, st[:]) { t.Fatalf("v2 stateid % x want % x", raw, st[:]) } if n, err := fd.Uint32(); err != nil || n != 1 { t.Fatalf("fhs %d: %v", n, err) } if v2fh, err := fd.VarOpaque(); err != nil || !bytes.Equal(v2fh, fh) { t.Fatalf("v2 fh % x", v2fh) } if _, _ = fd.String(); fd.Remaining() == 0 { // user t.Fatal("short v2 body") } if _, _ = fd.String(); fd.Remaining() == 0 { // group t.Fatal("short v2 body") } if flavor, err := fd.Uint32(); err != nil || flavor != 0 { t.Fatalf("auth flavor %d: %v", flavor, err) } if authBody, err := fd.VarOpaque(); err != nil || len(authBody) != 0 { t.Fatalf("auth body % x", authBody) } // GETDEVICEINFO and GETDEVICELIST accept the version two type. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutfh(nil, fh), nfs4.AppendGetDeviceInfoArgs(nil, layoutDeviceID, nfs4.LayoutTypeFlexFilesV2, 1<<16, nfs4.Bitmap{}), }) wantStatus(t, "ffv2 deviceinfo", res.Status, nfs4.ErrOK) seq++ res, _ = run(seq, [][]byte{ nfs4.AppendGetDeviceListArgs(nil, nfs4.LayoutTypeFlexFilesV2, 8, 0, [8]byte{}), }) wantStatus(t, "ffv2 devicelist", res.Status, nfs4.ErrOK) }