// 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" ) func TestOps42Matrix(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, "ops42", 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:] } // Open the source file and create the copy targets. seq++ res, bodies := run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendOpenArgs(nil, 0x5555, []byte("c42"), nfs4.ShareAccessBoth, 0, false, 0, "a.txt"), }) wantStatus(t, "open src", res.Status, nfs4.ErrOK) srcSt := bodyStateid(bodies[1]) seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendOpenArgs(nil, 0x5555, []byte("c42"), nfs4.ShareAccessBoth, 0, true, 0o644, "copy1"), nfs4.AppendGetfh(nil), nfs4.AppendPutRootfh(nil), nfs4.AppendOpenArgs(nil, 0x5555, []byte("c42"), nfs4.ShareAccessBoth, 0, true, 0o644, "clone1"), nfs4.AppendGetfh(nil), }) wantStatus(t, "open targets", res.Status, nfs4.ErrOK) copySt := bodyStateid(bodies[1]) copyFH, ferr := xdr.NewDecoder(bodies[2]).VarOpaque() if ferr != nil { t.Fatal(ferr) } cloneSt := bodyStateid(bodies[4]) cloneFH, ferr := xdr.NewDecoder(bodies[5]).VarOpaque() if ferr != nil { t.Fatal(ferr) } // COPY: saved fh is the source, current fh the destination. seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendSavefh(nil), nfs4.AppendPutfh(nil, copyFH), nfs4.AppendCopyArgs(nil, srcSt, copySt, 0, 0, 6, true, true, nil), }) wantStatus(t, "copy", res.Status, nfs4.ErrOK) cd := xdr.NewDecoder(bodies[4]) if n, cerr := cd.Uint32(); cerr != nil || n != 0 { t.Fatalf("callback array %d: %v", n, cerr) } if count, cerr := cd.Uint64(); cerr != nil || count != 6 { t.Fatalf("copied %d: %v", count, cerr) } data, rerr := h.FS.Read(copyFH, 0, 32) if rerr != nil || !bytes.Equal(data, []byte("hello ")) { t.Fatalf("copy content %q: %v", data, rerr) } // CLONE: the whole source into the target, ranges must fit. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendSavefh(nil), nfs4.AppendPutfh(nil, cloneFH), nfs4.AppendCloneArgs(nil, srcSt, cloneSt, 0, 0, 1000), }) if res.Status != nfs4.ErrInval { t.Fatalf("clone beyond size: status %d", res.Status) } // The clone target must be pre-extended to the range size, RFC 7862 // section 15.1.4. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutfh(nil, cloneFH), nfs4.AppendSetattrArgs(nil, nfs4.Stateid{}, nfs4.OfBits(nfs4.AttrSize), nfs4.Attrs{Size: 9}), }) wantStatus(t, "extend clone target", res.Status, nfs4.ErrOK) seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendSavefh(nil), nfs4.AppendPutfh(nil, cloneFH), nfs4.AppendCloneArgs(nil, srcSt, cloneSt, 0, 0, 9), }) wantStatus(t, "clone", res.Status, nfs4.ErrOK) data, rerr = h.FS.Read(cloneFH, 0, 32) if rerr != nil || !bytes.Equal(data, []byte("hello nfs")) { t.Fatalf("clone content %q: %v", data, rerr) } // READ_PLUS answers one data segment. seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendReadPlusArgs(nil, srcSt, 0, 32), }) wantStatus(t, "read plus", res.Status, nfs4.ErrOK) rd := xdr.NewDecoder(bodies[2]) if eof, eerr := rd.Bool(); eerr != nil || !eof { t.Fatalf("eof %v: %v", eof, eerr) } if n, _ := rd.Uint32(); n != 1 { t.Fatalf("contents %d", n) } if kind, _ := rd.Uint32(); kind != nfs4.ContentData { t.Fatalf("content kind %d", kind) } if _, err := rd.Uint64(); err != nil { t.Fatal(err) } pdata, perr := rd.VarOpaque() if perr != nil || !bytes.Equal(pdata, []byte("hello nfs")) { t.Fatalf("read plus data %q: %v", pdata, perr) } // IO_ADVISE echoes the hints; WRITE_SAME repeats the pattern. hints := nfs4.Bitmap{}.With(1) seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "copy1"), nfs4.AppendIoAdviseArgs(nil, copySt, 0, 100, hints), }) wantStatus(t, "io advise", res.Status, nfs4.ErrOK) seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "copy1"), nfs4.AppendWriteSameArgs(nil, copySt, nfs4.StableFileSync, 0, 4, 3, 0, 0, 0, []byte("ab")), }) wantStatus(t, "write same", res.Status, nfs4.ErrOK) data, rerr = h.FS.Read(copyFH, 0, 12) if rerr != nil || !bytes.Equal(data, []byte("abababababab")) { t.Fatalf("write same content %q: %v", data, rerr) } // OFFLOAD_STATUS without a request answers the dedicated status. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "copy1"), nfs4.AppendOffloadStatusArgs(nil, copySt), }) wantStatus(t, "offload status", res.Status, nfs4.ErrOffloadNoReqs) // OFFLOAD_CANCEL answers OK. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "copy1"), nfs4.AppendOffloadCancelArgs(nil, copySt), }) wantStatus(t, "offload cancel", res.Status, nfs4.ErrOK) // COPY_NOTIFY grants the copy with this server as the source. seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendCopyNotifyArgs(nil, srcSt, 3, "", nfs4.NetAddr{Netid: "tcp", Uaddr: "10.0.0.9.8.1"}), }) wantStatus(t, "copy notify", res.Status, nfs4.ErrOK) // LAYOUTERROR and LAYOUTSTATS accept the report. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendLayoutErrorArgs(nil, 0, 4096, srcSt, layoutDeviceID, nfs4.ErrBadStateid, nfs4.OpRead), }) wantStatus(t, "layout error", res.Status, nfs4.ErrOK) seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendLayoutStatsArgs(nil, 0, 4096, srcSt, 1, 9, 0, 0, layoutDeviceID, 0, nil), }) wantStatus(t, "layout stats", res.Status, nfs4.ErrOK) // SEEK: the backend has no hole in a.txt; a data seek finds offset 0 // and a hole seek past the end answers NOSPC. seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendSeekArgs(nil, srcSt, 3, nfs4.ContentData), }) wantStatus(t, "seek data", res.Status, nfs4.ErrOK) sd := xdr.NewDecoder(bodies[2]) if eof, _ := sd.Bool(); eof { t.Fatal("seek data reported eof") } if off, _ := sd.Uint64(); off != 3 { t.Fatalf("seek offset %d", off) } } func TestOps42RangesAndEdges(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, "rng", 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, 0x6666, []byte("rng"), nfs4.ShareAccessBoth, 0, false, 0, "a.txt"), }) wantStatus(t, "open", res.Status, nfs4.ErrOK) st := bodyStateid(bodies[1]) // ALLOCATE reserves, DEALLOCATE releases; a zero length is refused. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendAllocateArgs(nil, st, 0, 64), }) wantStatus(t, "allocate", res.Status, nfs4.ErrOK) seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendDeallocateArgs(nil, st, 0, 64), }) wantStatus(t, "deallocate", res.Status, nfs4.ErrOK) seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendDeallocateArgs(nil, st, 0, 0), }) wantStatus(t, "deallocate zero", res.Status, nfs4.ErrInval) // SEEK with a nonsense content kind answers INVAL. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendSeekArgs(nil, st, 0, 9), }) wantStatus(t, "seek nonsense", res.Status, nfs4.ErrInval) // COPY with named source servers decodes the netloc list. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendSavefh(nil), nfs4.AppendPutfh(nil, mustLookup(t, h, "a.txt")), nfs4.AppendCopyArgs(nil, st, nfs4.Stateid{}, 0, 0, 0, false, true, []nfs4.CopySourceServer{{Type: 1, Name: "src.example.org"}}), }) wantStatus(t, "copy zero length", res.Status, nfs4.ErrOK) // REMOVEXATTR of a missing attribute answers NOXATTR. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendRemoveXattrArgs(nil, "user.nothere"), }) wantStatus(t, "remove missing xattr", res.Status, nfs4.ErrNoXattr) } func TestOps42WithoutFileHandle(t *testing.T) { h := testTree(t) sid, seq := newSession(t, h) // Every 4.2 data operation refuses to run without a current file // handle; a table keeps the coverage of the guard honest. table := []struct { name string op []byte }{ {"seek", nfs4.AppendSeekArgs(nil, nfs4.Stateid{}, 0, nfs4.ContentData)}, {"allocate", nfs4.AppendAllocateArgs(nil, nfs4.Stateid{}, 0, 8)}, {"deallocate", nfs4.AppendDeallocateArgs(nil, nfs4.Stateid{}, 0, 8)}, {"ioadvise", nfs4.AppendIoAdviseArgs(nil, nfs4.Stateid{}, 0, 8, nfs4.Bitmap{})}, {"copy", nfs4.AppendCopyArgs(nil, nfs4.Stateid{}, nfs4.Stateid{}, 0, 0, 0, false, true, nil)}, {"copynotify", nfs4.AppendCopyNotifyArgs(nil, nfs4.Stateid{}, 2, "dst", nfs4.NetAddr{})}, {"offloadcancel", nfs4.AppendOffloadCancelArgs(nil, nfs4.Stateid{})}, {"offloadstatus", nfs4.AppendOffloadStatusArgs(nil, nfs4.Stateid{})}, {"layouterror", nfs4.AppendLayoutErrorArgs(nil, 0, 0, nfs4.Stateid{}, layoutDeviceID, 0, 0)}, {"layoutstats", nfs4.AppendLayoutStatsArgs(nil, 0, 0, nfs4.Stateid{}, 0, 0, 0, 0, layoutDeviceID, 0, nil)}, {"readplus", nfs4.AppendReadPlusArgs(nil, nfs4.Stateid{}, 0, 8)}, {"writesame", nfs4.AppendWriteSameArgs(nil, nfs4.Stateid{}, nfs4.StableFileSync, 0, 8, 1, 0, 0, 0, []byte("x"))}, } for _, row := range table { seq++ all := append([][]byte{nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true)}, row.op) payload := nfs4.AppendCompoundArgs(nil, "no-fh", nfs4.MinorVersion, all) body, ok := h.compound(payload, cred{uid: 0, gid: 0}) if !ok { t.Fatalf("%s: garbage", row.name) } res, _, err := nfs4.DecodeCompoundResBodies(body) if err != nil { t.Fatalf("%s: decode %v", row.name, err) } if res.Status != nfs4.ErrNoFileHandle { t.Fatalf("%s: status %d, want NOFILE_HANDLE", row.name, res.Status) } } } func TestSeekUnderLockStateid(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, "lockseek", 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, 0x7777, []byte("ls"), nfs4.ShareAccessBoth, 0, false, 0, "a.txt"), }) wantStatus(t, "open", res.Status, nfs4.ErrOK) openSt := bodyStateid(bodies[1]) seq++ res, bodies = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendLockArgsNew(nil, openSt, 0x7777, []byte("locker"), nfs4.LockTypeWrite, false, 0, 9), }) wantStatus(t, "lock", res.Status, nfs4.ErrOK) lockSt := bodyStateid(bodies[2]) // A data operation under the LOCK stateid is legal; the store falls // back from the open store to the lock store. seq++ res, _ = run(seq, [][]byte{ nfs4.AppendPutRootfh(nil), nfs4.AppendLookup(nil, "a.txt"), nfs4.AppendSeekArgs(nil, lockSt, 0, nfs4.ContentData), }) wantStatus(t, "seek under lock", res.Status, nfs4.ErrOK) }