Files
nfs/internal/nfs4server/xattr_test.go
T
petrbalvin a9b8039ef7
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
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

180 lines
5.3 KiB
Go

// 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)
}
}
}