// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package nfs4 import ( "bytes" "testing" "sourcedock.dev/petrbalvin/nfs/internal/xdr" ) // The location attributes round trip: the fs_locations attribute // decodes back to the servers and root paths, and the info attribute // carries the flags, the validity window and the replica entries. func TestFsLocationsShapes(t *testing.T) { blob := AppendFsLocations(nil, []string{"exports"}, []FsLocation{{ Servers: []string{"a.example", "b.example"}, RootPath: []string{"exports", "data"}, }}) root, locations, err := DecodeFsLocations(blob) if err != nil { t.Fatal(err) } if len(root) != 1 || root[0] != "exports" { t.Fatalf("root %v", root) } if len(locations) != 1 || len(locations[0].Servers) != 2 || locations[0].Servers[1] != "b.example" || len(locations[0].RootPath) != 2 || locations[0].RootPath[1] != "data" { t.Fatalf("locations %+v", locations) } // The fs_locations_info attribute: flags, validity, items with the // opaque info bytes. info := AppendFsLocationsInfo(nil, Fsli4ifVarSub, 60, []string{"exports"}, []FsItem{{Entries: []FsServer{{ Currency: 3, Info: []byte{Fsli4gfWritable | Fsli4gfCurReq}, Server: "a.example", }}, RootPath: []string{"data"}}}) d := xdr.NewDecoder(info) if flags, err := d.Uint32(); err != nil || flags != Fsli4ifVarSub { t.Fatalf("flags %d: %v", flags, err) } if valid, err := d.Int32(); err != nil || valid != 60 { t.Fatalf("valid %d: %v", valid, err) } if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("root count %d: %v", n, err) } if rootComponent, err := d.String(); err != nil || rootComponent != "exports" { t.Fatalf("root component %q: %v", rootComponent, err) } if _, err := d.Uint32(); err != nil { // items count t.Fatal(err) } if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("entries %d: %v", n, err) } if cur, err := d.Int32(); err != nil || cur != 3 { t.Fatalf("currency %d: %v", cur, err) } if info2, err := d.VarOpaque(); err != nil || len(info2) != 1 || info2[0]&Fsli4gfWritable == 0 || info2[0]&Fsli4gfCurReq == 0 { t.Fatalf("info % x: %v", info2, err) } if s, err := d.String(); err != nil || s != "a.example" { t.Fatalf("server %q: %v", s, err) } // The item closes with the root path of the replicas. if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("rootpath count %d: %v", n, err) } if c, err := d.String(); err != nil || c != "data" { t.Fatalf("rootpath %q: %v", c, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left", d.Remaining()) } } // The emulated layout family bodies carry the device id first and the // family payload after; the GETDEVICELIST pair round trips. func TestLayoutFamilyShapes(t *testing.T) { var dev [16]byte copy(dev[:], "0123456789abcdef") files := AppendFileLayoutBody(nil, dev, 4096, 2, 8, [][]byte{[]byte("fh1"), []byte("fh2")}) d := xdr.NewDecoder(files) if got, err := d.Raw(16); err != nil || !bytes.Equal(got, dev[:]) { t.Fatalf("device % x: %v", got, err) } if u, err := d.Uint32(); err != nil || u != 4096 { t.Fatalf("util %d: %v", u, err) } if fs, err := d.Uint32(); err != nil || fs != 2 { t.Fatalf("first stripe %d: %v", fs, err) } if po, err := d.Uint64(); err != nil || po != 8 { t.Fatalf("pattern %d: %v", po, err) } if n, err := d.Uint32(); err != nil || n != 2 { t.Fatalf("fh count %d: %v", n, err) } if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh1" { t.Fatalf("fh %q: %v", fh, err) } if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh2" { t.Fatalf("fh2 %q: %v", fh, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left", d.Remaining()) } // The device address: stripe indices and the multipath list. addr := AppendFileDeviceAddr(nil, []uint32{0, 1}, []NetAddr{{Netid: "tcp", Uaddr: "10.0.0.1.8.1"}}) d = xdr.NewDecoder(addr) if n, err := d.Uint32(); err != nil || n != 2 { t.Fatalf("indices %d: %v", n, err) } if _, err := d.Raw(8); err != nil { t.Fatal(err) } if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("addrs %d: %v", n, err) } if id, err := d.String(); err != nil || id != "tcp" { t.Fatalf("netid %q: %v", id, err) } if ua, err := d.String(); err != nil || ua != "10.0.0.1.8.1" { t.Fatalf("uaddr %q: %v", ua, err) } // The block volume, the SCSI extent and the OSD component bodies // carry their shapes. block := AppendBlockDeviceAddr(nil, BlockVolume{DeviceID: dev, BaseOffset: 8, BlockCount: 64}) d = xdr.NewDecoder(block) if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("volumes %d: %v", n, err) } if typ, err := d.Uint32(); err != nil || typ != 1 { t.Fatalf("volume type %d: %v", typ, err) } raw, err := d.Raw(16) if err != nil || !bytes.Equal(raw, dev[:]) { t.Fatalf("volume device % x: %v", raw, err) } if bo, err := d.Uint64(); err != nil || bo != 8 { t.Fatalf("base offset %d: %v", bo, err) } if bc, err := d.Uint64(); err != nil || bc != 64 { t.Fatalf("block count %d: %v", bc, err) } scsi := AppendScsiLayoutBody(nil, dev, 1, 2, 3) d = xdr.NewDecoder(scsi) if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("extents %d: %v", n, err) } raw, err = d.Raw(16) if err != nil || !bytes.Equal(raw, dev[:]) { t.Fatalf("extent device % x: %v", raw, err) } for _, want := range []uint64{1, 2, 3} { if v, err := d.Uint64(); err != nil || v != want { t.Fatalf("extent field %d: %v", v, err) } } obj := AppendObjectLayoutBody(nil, dev, ObjectLayout{NumComponents: 1, StripeUnit: 4096, GroupWidth: 1, GroupDepth: 1, RefTagSize: 4}) d = xdr.NewDecoder(obj) if alg, err := d.Uint32(); err != nil || alg != 0 { t.Fatalf("raid algorithm %d: %v", alg, err) } if su, err := d.Uint64(); err != nil || su != 4096 { t.Fatalf("stripe unit %d: %v", su, err) } // The GETDEVICELIST pair round trips. list := AppendGetDeviceListArgs(nil, LayoutTypeFlexfiles, 8, 5, [8]byte{9}) d = xdr.NewDecoder(list[4:]) if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles { t.Fatalf("list type %d: %v", typ, err) } if md, err := d.Uint32(); err != nil || md != 8 { t.Fatalf("max devices %d: %v", md, err) } if c, err := d.Uint64(); err != nil || c != 5 { t.Fatalf("cookie %d: %v", c, err) } raw, err = d.Raw(8) if err != nil || raw[0] != 9 { t.Fatalf("verifier % x: %v", raw, err) } res := AppendGetDeviceListRes(nil, 5, [8]byte{9}, [][16]byte{dev, {1}}, true) d = xdr.NewDecoder(res) if c, err := d.Uint64(); err != nil || c != 5 { t.Fatalf("res cookie %d: %v", c, err) } if _, err := d.Raw(8); err != nil { t.Fatal(err) } if n, err := d.Uint32(); err != nil || n != 2 { t.Fatalf("devices %d: %v", n, err) } if _, err := d.Raw(32); err != nil { t.Fatal(err) } if eof, err := d.Bool(); err != nil || !eof { t.Fatalf("eof %v: %v", eof, err) } } // The OPENATTR and READ argop encoders carry their shapes. func TestOpenattrReadArgs(t *testing.T) { oa := AppendOpenattrArgs(nil, true) d := xdr.NewDecoder(oa[4:]) if created, err := d.Bool(); err != nil || !created { t.Fatalf("created %v: %v", created, err) } ra := AppendReadArgs(nil, Stateid{7}, 99, 1024) d = xdr.NewDecoder(ra[4:]) raw, err := d.Raw(16) if err != nil || raw[0] != 7 { t.Fatalf("stateid % x: %v", raw, err) } if off, err := d.Uint64(); err != nil || off != 99 { t.Fatalf("offset %d: %v", off, err) } if cnt, err := d.Uint32(); err != nil || cnt != 1024 { t.Fatalf("count %d: %v", cnt, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left", d.Remaining()) } } // The flexfiles version 2 layout body of // draft-haynes-nfsv4-flex-filesv2-00 round trips: the data server // carries the stateid array, the file handle list and the RPC // credential the draft adds. func TestFlexFileLayoutV2Shape(t *testing.T) { var dev [16]byte copy(dev[:], "v2flexdeviceid!!") st := Stateid{2, 'F', 'F', 'V', '2'} body := AppendFlexFileLayoutBodyV2(nil, 3, 0, []FlexMirrorV2{{ DataServers: []FlexDataServerV2{{ DeviceID: dev, Efficiency: 7, Stateids: []Stateid{st}, FHs: [][]byte{[]byte("fh-v2")}, User: "petr", Group: "nfs", AuthFlavor: 0, AuthBody: nil, }}, }}) d := xdr.NewDecoder(body) if su, err := d.Uint64(); err != nil || su != 0 { t.Fatalf("stripe unit %d: %v", su, err) } if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("mirrors %d: %v", n, err) } if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("servers %d: %v", n, err) } raw, err := d.Raw(16) if err != nil || !bytes.Equal(raw, dev[:]) { t.Fatalf("device % x: %v", raw, err) } if eff, err := d.Uint32(); err != nil || eff != 7 { t.Fatalf("efficiency %d: %v", eff, err) } if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("stateids %d: %v", n, err) } raw, err = d.Raw(16) if err != nil || !bytes.Equal(raw, st[:]) { t.Fatalf("stateid % x: %v", raw, err) } if n, err := d.Uint32(); err != nil || n != 1 { t.Fatalf("fhs %d: %v", n, err) } if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh-v2" { t.Fatalf("fh %q: %v", fh, err) } if u, err := d.String(); err != nil || u != "petr" { t.Fatalf("user %q: %v", u, err) } if g, err := d.String(); err != nil || g != "nfs" { t.Fatalf("group %q: %v", g, err) } if flavor, err := d.Uint32(); err != nil || flavor != 0 { t.Fatalf("auth flavor %d: %v", flavor, err) } if authBody, err := d.VarOpaque(); err != nil || len(authBody) != 0 { t.Fatalf("auth body % x: %v", authBody, err) } if flags, err := d.Uint32(); err != nil || flags != 3 { t.Fatalf("flags %d: %v", flags, err) } if hint, err := d.Uint32(); err != nil || hint != 0 { t.Fatalf("stats hint %d: %v", hint, err) } if d.Remaining() != 0 { t.Fatalf("%d bytes left", d.Remaining()) } }