feat: full NFSv4.2 server and client in pure Go
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package nfs4server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfs4"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/nfsfs"
|
||||
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
|
||||
)
|
||||
|
||||
func TestXattrOverCompound(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, "xattr", 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, _ := run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendGetXattrArgs(nil, "user.comment"),
|
||||
})
|
||||
wantStatus(t, "get before set", res.Status, nfs4.ErrNoXattr)
|
||||
|
||||
// Set, read back, list, remove: the whole life of one attribute.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSetXattrArgs(nil, nfs4.SetXattrUnchecked, "user.comment", []byte("hello")),
|
||||
})
|
||||
wantStatus(t, "set", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, bodies := run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendGetXattrArgs(nil, "user.comment"),
|
||||
})
|
||||
wantStatus(t, "get", res.Status, nfs4.ErrOK)
|
||||
value, gerr := nfs4.DecodeGetXattrRes(bodies[2])
|
||||
if gerr != nil || string(value) != "hello" {
|
||||
t.Fatalf("value %q: %v", value, gerr)
|
||||
}
|
||||
|
||||
// CREATE on a live attribute is refused; REPLACE on it succeeds.
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSetXattrArgs(nil, nfs4.SetXattrCreate, "user.comment", []byte("again")),
|
||||
})
|
||||
if res.Status == nfs4.ErrOK {
|
||||
t.Fatal("create over a live attribute succeeded")
|
||||
}
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendSetXattrArgs(nil, nfs4.SetXattrReplace, "user.comment", []byte("second")),
|
||||
})
|
||||
wantStatus(t, "replace", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, bodies = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendListXattrArgs(nil, 0, 1<<16),
|
||||
})
|
||||
wantStatus(t, "list", res.Status, nfs4.ErrOK)
|
||||
ld := xdr.NewDecoder(bodies[2])
|
||||
if ck, lerr := ld.Uint64(); lerr != nil || ck != 0 {
|
||||
t.Fatalf("cookie %d: %v", ck, lerr)
|
||||
}
|
||||
n, lerr := ld.Uint32()
|
||||
if lerr != nil || n != 1 {
|
||||
t.Fatalf("names %d: %v", n, lerr)
|
||||
}
|
||||
name, lerr := ld.String()
|
||||
if lerr != nil || name != "user.comment" {
|
||||
t.Fatalf("name %q: %v", name, lerr)
|
||||
}
|
||||
if eof, lerr := ld.Bool(); lerr != nil || !eof {
|
||||
t.Fatalf("eof %v: %v", eof, lerr)
|
||||
}
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendRemoveXattrArgs(nil, "user.comment"),
|
||||
})
|
||||
wantStatus(t, "remove", res.Status, nfs4.ErrOK)
|
||||
seq++
|
||||
res, _ = run(seq, [][]byte{
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendGetXattrArgs(nil, "user.comment"),
|
||||
})
|
||||
wantStatus(t, "get after remove", res.Status, nfs4.ErrNoXattr)
|
||||
}
|
||||
|
||||
// A bare FS wrapper hides the backend's xattr half: the xattr family must
|
||||
// answer NOT_SUPP against it.
|
||||
type bareFS struct {
|
||||
nfsfs.FS
|
||||
}
|
||||
|
||||
func TestXattrWithoutBackend(t *testing.T) {
|
||||
h := testTree(t)
|
||||
h.FS = bareFS{h.FS}
|
||||
sid, seq := newSession(t, h)
|
||||
all := [][]byte{
|
||||
nfs4.AppendSequenceArgs(nil, sid, seq, 0, defaultSlots-1, true),
|
||||
nfs4.AppendPutRootfh(nil),
|
||||
nfs4.AppendLookup(nil, "a.txt"),
|
||||
nfs4.AppendGetXattrArgs(nil, "user.comment"),
|
||||
nfs4.AppendSetXattrArgs(nil, nfs4.SetXattrUnchecked, "user.comment", []byte("v")),
|
||||
nfs4.AppendListXattrArgs(nil, 0, 1<<16),
|
||||
nfs4.AppendRemoveXattrArgs(nil, "user.comment"),
|
||||
}
|
||||
payload := nfs4.AppendCompoundArgs(nil, "xattr", nfs4.MinorVersion, all)
|
||||
body, ok := h.compound(payload, cred{uid: 0, gid: 0})
|
||||
if !ok {
|
||||
t.Fatal("garbage")
|
||||
}
|
||||
res, _, err := nfs4.DecodeCompoundResBodies(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// ops: sequence, putrootfh, lookup, then four xattr ops.
|
||||
for i, op := range res.Ops[3:] {
|
||||
if op.Status != nfs4.ErrNotSupp {
|
||||
t.Fatalf("xattr op %d: status %d, want NOT_SUPP", i, op.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXattrWithoutFileHandle(t *testing.T) {
|
||||
h := testTree(t)
|
||||
sid, seq := newSession(t, h)
|
||||
table := []struct {
|
||||
name string
|
||||
op []byte
|
||||
}{
|
||||
{"get", nfs4.AppendGetXattrArgs(nil, "user.a")},
|
||||
{"set", nfs4.AppendSetXattrArgs(nil, nfs4.SetXattrUnchecked, "user.a", []byte("v"))},
|
||||
{"list", nfs4.AppendListXattrArgs(nil, 0, 1<<16)},
|
||||
{"remove", nfs4.AppendRemoveXattrArgs(nil, "user.a")},
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user