180 lines
5.3 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|