Files
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

861 lines
29 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"errors"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestBitmapBits(t *testing.T) {
m := OfBits(0, 31, 32, 55, 75)
for _, n := range []uint32{0, 31, 32, 55, 75} {
if !m.Has(n) {
t.Fatalf("bit %d is not set", n)
}
}
for _, n := range []uint32{1, 30, 33, 54, 76} {
if m.Has(n) {
t.Fatalf("bit %d is set but was never given", n)
}
}
}
func TestBitmapWireRoundTrip(t *testing.T) {
want := OfBits(1, 3, 33, 55)
d := xdr.NewDecoder(want.AppendTo(nil))
got, err := ReadBitmap(d)
if err != nil {
t.Fatalf("read: %v", err)
}
for n := range uint32(80) {
if want.Has(n) != got.Has(n) {
t.Fatalf("bit %d: %v, want %v", n, got.Has(n), want.Has(n))
}
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left over", d.Remaining())
}
}
func TestBitmapEmptyAndHuge(t *testing.T) {
if len(OfBits().AppendTo(nil)) != 4 {
t.Fatal("an empty bitmap encodes as exactly the zero word count")
}
huge := xdr.AppendUint32(nil, 101)
if _, err := ReadBitmap(xdr.NewDecoder(huge)); !errors.Is(err, xdr.ErrBadLength) {
t.Fatalf("a 101 word bitmap: %v", err)
}
}
func TestStateidWire(t *testing.T) {
b := AllZero.Append(nil)
if len(b) != 16 || !bytes.Equal(b, make([]byte, 16)) {
t.Fatalf("the anonymous stateid encodes as %x", b)
}
var s Stateid
s[0] = 0x80
if got := s.Append(nil); !bytes.Equal(got, s[:]) {
t.Fatal("a stateid encodes in its own byte order")
}
}
func TestFattrAllAdvertised(t *testing.T) {
// The whole advertised set, every field set, in both directions.
request := append(Bitmap{}, SupportedAttrs...)
want := Attrs{
Type: NF4Dir,
FHExpireType: FH4Persistent,
Change: 7,
Size: 1,
LinkSupport: true,
SymlinkSupport: true,
NamedAttr: false,
FSID: [2]uint64{9, 0},
UniqueHandles: true,
FileHandle: []byte("h"),
FileID: 3,
Mode: 0o755,
Numlinks: 2,
SpaceUsed: 4096,
TimeAccess: NfsTime{1, 2},
TimeMetadata: NfsTime{3, 4},
TimeModify: NfsTime{5, 6},
MountedOnFileID: 3,
Limits: DefaultLimits,
}
response, got, err := DecodeGetattrBody(AppendGetattrRes(nil, request, want))
if err != nil {
t.Fatalf("decode: %v", err)
}
for _, n := range []uint32{AttrSupportedAttrs, AttrType, AttrFHExpireType, AttrChange,
AttrSize, AttrLinkSupport, AttrSymlinkSupport, AttrNamedAttr, AttrFSID,
AttrUniqueHandles, AttrFileHandle, AttrFileID, AttrMaxName, AttrMaxRead,
AttrMaxWrite, AttrMode, AttrNumlinks, AttrSpaceUsed, AttrTimeAccess,
AttrTimeMetadata, AttrTimeModify, AttrMountedOnFileID} {
if !response.Has(n) {
t.Fatalf("attribute %d missing from the response bitmap", n)
}
}
if got.Type != want.Type || got.FHExpireType != want.FHExpireType || got.Change != want.Change ||
got.Size != want.Size || got.LinkSupport != want.LinkSupport ||
got.SymlinkSupport != want.SymlinkSupport || got.NamedAttr != want.NamedAttr ||
got.FSID != want.FSID || got.UniqueHandles != want.UniqueHandles ||
!bytes.Equal(got.FileHandle, want.FileHandle) || got.FileID != want.FileID ||
got.Mode != want.Mode || got.Numlinks != want.Numlinks ||
got.SpaceUsed != want.SpaceUsed || got.TimeAccess != want.TimeAccess ||
got.TimeMetadata != want.TimeMetadata || got.TimeModify != want.TimeModify ||
got.MountedOnFileID != want.MountedOnFileID || got.Limits != want.Limits {
t.Fatalf("round trip mismatch:\n got %+v\nwant %+v", got, want)
}
}
func TestGetattrTruncatedBody(t *testing.T) {
// A successful GETATTR whose blob is cut short is a decoding error at
// the exact attribute, not a silent misread.
request := OfBits(AttrType, AttrSize)
full := AppendGetattrRes(nil, request, Attrs{Type: NF4Reg, Size: 5})
// The fattr4 is a bitmap then a length prefixed blob; cut the last
// byte off the whole result and the blob no longer holds the size.
truncated := full[:len(full)-1]
if _, _, err := DecodeGetattrBody(truncated); err == nil {
t.Fatal("a truncated attribute list decoded")
}
}
func TestBitmapWithAndWords(t *testing.T) {
m := OfBits(3).With(70)
if !m.Has(3) || !m.Has(70) {
t.Fatalf("With lost a bit: %v", m.Words())
}
if len(OfBits(3, 32, 64).Words()) != 3 {
t.Fatal("Words trimmed a live word")
}
if len(Bitmap{5, 0}.Words()) != 1 {
t.Fatal("Words kept a trailing zero word")
}
}
func TestCompoundResUnknownOpBody(t *testing.T) {
// A successful result of an operation this client cannot measure is a
// decoding error, not a silent misread. WANT_DELEGATION fits: this
// build neither asks for delegations nor understands their answer.
payload := AppendCompoundRes(nil, ErrOK, "", [][]byte{
AppendOpHeader(nil, OpWantDelegation, ErrOK),
})
if _, _, err := DecodeCompoundResBodies(payload); !errors.Is(err, ErrUnknownOp) {
t.Fatalf("a WANT_DELEGATION result decoded as %v", err)
}
}
func TestAccessResRoundTrip(t *testing.T) {
supported, access, err := DecodeAccessBody(AppendAccessRes(nil, 0x3f, 0x21))
if err != nil || supported != 0x3f || access != 0x21 {
t.Fatalf("access body: %x %x, %v", supported, access, err)
}
}
func TestArgopBuilders(t *testing.T) {
// Every builder writes the operation number first, then its arguments.
cases := []struct {
name string
arg []byte
op uint32
}{
{"putrootfh", AppendPutRootfh(nil), OpPutRootfh},
{"savefh", AppendSavefh(nil), OpSavefh},
{"restorefh", AppendRestorefh(nil), OpRestorefh},
{"getfh", AppendGetfh(nil), OpGetfh},
{"putfh", AppendPutfh(nil, []byte("h")), OpPutfh},
{"lookup", AppendLookup(nil, "n"), OpLookup},
{"getattr", AppendGetattr(nil, OfBits(AttrType)), OpGetattr},
{"access", AppendAccess(nil, AccessRead), OpAccess},
{"read", AppendRead(nil, AllZero, 1, 2), OpRead},
{"readdir", AppendReaddir(nil, 1, [8]byte{}, 8, 4096, OfBits(AttrType)), OpReaddir},
}
for _, tc := range cases {
d := xdr.NewDecoder(tc.arg)
op, err := d.Uint32()
if err != nil || op != tc.op {
t.Fatalf("%s: op %d, %v; want %d", tc.name, op, err, tc.op)
}
}
// Spot check the argument shapes the tests rely on.
if fh, err := xdr.NewDecoder(AppendPutfh(nil, []byte("h"))[4:]).VarOpaque(); err != nil || string(fh) != "h" {
t.Fatalf("putfh argument: %q, %v", fh, err)
}
if n, err := xdr.NewDecoder(AppendLookup(nil, "n")[4:]).String(); err != nil || n != "n" {
t.Fatalf("lookup argument: %q, %v", n, err)
}
}
func TestResBodyLen(t *testing.T) {
getattr := AppendGetattrRes(nil, OfBits(AttrType), Attrs{Type: NF4Reg})
readdir := AppendReadDirRes(nil, [8]byte{}, []DirEntryRes{{Cookie: 1, Name: "x", Attrs: Attrs{Type: NF4Reg}}}, OfBits(AttrType), true)
cases := []struct {
name string
op uint32
status uint32
body []byte
want int
}{
{"error has no body", OpGetfh, ErrNoEnt, []byte{9, 9, 9, 9}, 0},
{"putfh", OpPutfh, ErrOK, nil, 0},
{"getfh", OpGetfh, ErrOK, AppendGetfhRes(nil, []byte("handle")), 4 + 8},
{"open", OpOpen, ErrOK, AppendOpenRes(nil, AllZero), 16 + 20 + 4 + 4 + 4},
{"getattr", OpGetattr, ErrOK, getattr, len(getattr)},
{"access", OpAccess, ErrOK, AppendAccessRes(nil, 3, 1), 8},
{"read", OpRead, ErrOK, AppendReadRes(nil, false, []byte("ab")), 4 + 4 + 2 + 2},
{"readdir", OpReaddir, ErrOK, readdir, len(readdir)},
}
for _, tc := range cases {
got, err := resBodyLen(tc.op, tc.status, tc.body)
if err != nil || got != tc.want {
t.Fatalf("%s: %d, %v; want %d", tc.name, got, err, tc.want)
}
}
if _, err := resBodyLen(OpWantDelegation, ErrOK, nil); !errors.Is(err, ErrUnknownOp) {
t.Fatalf("an unmeasurable result: %v", err)
}
if n, err := resBodyLen(OpGetfh, ErrOK, []byte{0, 0, 0, 9, 1}); err == nil || n != 0 {
t.Fatalf("a truncated getfh body measured as %d, %v", n, err)
}
}
func TestWriteWire(t *testing.T) {
args := AppendWriteArgs(nil, AllZero, 512, StableDataSync, []byte("data"))
stateid, off, stable, data, err := DecodeWriteArgs(args[4:])
if err != nil {
t.Fatalf("decode: %v", err)
}
if stateid != AllZero || off != 512 || stable != StableDataSync || string(data) != "data" {
t.Fatalf("write args: %+v %d %d %q", stateid, off, stable, data)
}
verf := [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
res := AppendWriteRes(nil, 4, StableFileSync, verf)
count, committed, gotVerf, err := DecodeWriteRes(res)
if err != nil || count != 4 || committed != StableFileSync || gotVerf != verf {
t.Fatalf("write res: %d %d %x, %v", count, committed, gotVerf, err)
}
// The measured body length matches what was written.
n, err := resBodyLen(OpWrite, ErrOK, res)
if err != nil || n != len(res) {
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
}
}
func TestCreateWire(t *testing.T) {
cases := []struct {
name string
kind uint32
linkdata string
major uint32
minor uint32
perm uint32
}{
{"dir", NF4Dir, "", 0, 0, 0o750},
{"symlink", NF4Lnk, "target.txt", 0, 0, 0o644},
{"fifo", NF4Fifo, "", 0, 0, 0o600},
{"chrdev", NF4Chr, "", 4, 64, 0o600},
{"nomode", NF4Sock, "", 0, 0, 0},
}
for _, tc := range cases {
arg := AppendCreateArgs(nil, tc.kind, "obj", tc.linkdata, tc.major, tc.minor, tc.perm)
kind, name, linkdata, major, minor, perm, err := DecodeCreateArgs(arg[4:])
if err != nil {
t.Fatalf("%s: %v", tc.name, err)
}
if kind != tc.kind || name != "obj" || linkdata != tc.linkdata ||
major != tc.major || minor != tc.minor || perm != tc.perm {
t.Fatalf("%s: kind %d name %q linkdata %q major %d minor %d perm %o",
tc.name, kind, name, linkdata, major, minor, perm)
}
}
}
func TestCreateResMeasure(t *testing.T) {
res := AppendCreateRes(nil)
n, err := resBodyLen(OpCreate, ErrOK, res)
if err != nil || n != len(res) {
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
}
}
func TestFattrRoundTrip(t *testing.T) {
request := OfBits(AttrType, AttrChange, AttrSize, AttrFSID, AttrMode,
AttrNumlinks, AttrTimeModify, AttrMountedOnFileID, AttrFileHandle)
a := Attrs{
Type: NF4Reg,
FHExpireType: FH4Persistent,
Change: 0x1234,
Size: 9,
LinkSupport: true,
SymlinkSupport: true,
NamedAttr: false,
FSID: [2]uint64{0x700, 0},
UniqueHandles: true,
FileHandle: []byte("handle-bytes"),
FileID: 42,
Mode: 0o644,
Numlinks: 1,
SpaceUsed: 4096,
TimeModify: NfsTime{1700000000, 500},
MountedOnFileID: 42,
}
response, got, err := DecodeGetattrBody(AppendGetattrRes(nil, request, a))
if err != nil {
t.Fatalf("decode: %v", err)
}
for _, n := range []uint32{AttrType, AttrChange, AttrSize, AttrFSID, AttrMode,
AttrNumlinks, AttrTimeModify, AttrMountedOnFileID, AttrFileHandle} {
if !response.Has(n) {
t.Fatalf("attribute %d is missing from the response bitmap", n)
}
}
if got.Type != NF4Reg || got.Change != a.Change || got.Size != 9 ||
got.FSID != a.FSID || got.Mode != 0o644 || got.Numlinks != 1 ||
got.TimeModify != a.TimeModify || got.MountedOnFileID != 42 ||
!bytes.Equal(got.FileHandle, a.FileHandle) {
t.Fatalf("round trip mismatch: %+v", got)
}
}
func TestFattrSkipsUnsupported(t *testing.T) {
// Owner is encoded when the attributes carry it: a request that
// asks gets the decimal uid string in the blob and the response
// bitmap keeps it.
request := OfBits(AttrType, AttrOwner)
response, got, err := DecodeGetattrBody(AppendGetattrRes(nil, request, Attrs{Type: NF4Dir, Owner: "0"}))
if err != nil {
t.Fatalf("decode: %v", err)
}
if !response.Has(AttrType) || !response.Has(AttrOwner) {
t.Fatalf("response bitmap %v drops the owner", response.Words())
}
if got.Type != NF4Dir || got.Owner != "0" {
t.Fatalf("type %d owner %q", got.Type, got.Owner)
}
}
func TestFattrEmptyRequest(t *testing.T) {
response, _, err := DecodeGetattrBody(AppendGetattrRes(nil, Bitmap{}, Attrs{}))
if err != nil || len(response.Words()) != 0 {
t.Fatalf("an empty request: %v, %v", response, err)
}
}
func TestCompoundArgsRoundTrip(t *testing.T) {
var ops [][]byte
ops = append(ops, AppendPutRootfh(nil))
ops = append(ops, AppendPutfh(nil, []byte{1, 2, 3}))
ops = append(ops, AppendLookup(nil, "a.txt"))
payload := AppendCompoundArgs(nil, "probe", 2, ops)
h, d, err := DecodeCompoundArgs(payload)
if err != nil {
t.Fatalf("decode: %v", err)
}
if h.Tag != "probe" || h.Minor != 2 || h.OpCount != 3 {
t.Fatalf("header %+v", h)
}
for i, want := range []uint32{OpPutRootfh, OpPutfh, OpLookup} {
got, err := d.Uint32()
if err != nil || got != want {
t.Fatalf("op %d: got %d, %v; want %d", i, got, err, want)
}
if want == OpPutfh {
if fh, err := d.VarOpaque(); err != nil || !bytes.Equal(fh, []byte{1, 2, 3}) {
t.Fatalf("putfh argument: %x, %v", fh, err)
}
}
if want == OpLookup {
if name, err := d.String(); err != nil || name != "a.txt" {
t.Fatalf("lookup component: %q, %v", name, err)
}
}
}
}
func TestCompoundResBodies(t *testing.T) {
var results [][]byte
results = append(results, AppendOpHeader(nil, OpPutRootfh, ErrOK))
fh := AppendGetfhRes(AppendOpHeader(nil, OpGetfh, ErrOK), []byte("the-handle"))
results = append(results, fh)
results = append(results, AppendOpHeader(nil, OpLookup, ErrNoEnt))
payload := AppendCompoundRes(nil, ErrNoEnt, "tag", results)
res, bodies, err := DecodeCompoundResBodies(payload)
if err != nil {
t.Fatalf("decode: %v", err)
}
if res.Status != ErrNoEnt || res.Tag != "tag" || len(res.Ops) != 3 {
t.Fatalf("res: status %d tag %q ops %d", res.Status, res.Tag, len(res.Ops))
}
wantStatus := []uint32{ErrOK, ErrOK, ErrNoEnt}
for i, op := range res.Ops {
if op.Status != wantStatus[i] {
t.Fatalf("op %d status %d", i, op.Status)
}
}
if len(bodies[0]) != 0 || len(bodies[2]) != 0 {
t.Fatalf("a bodyless result carried bytes: %x %x", bodies[0], bodies[2])
}
gotFH, err := xdr.NewDecoder(bodies[1]).VarOpaque()
if err != nil || !bytes.Equal(gotFH, []byte("the-handle")) {
t.Fatalf("getfh body: %q, %v", gotFH, err)
}
}
func TestReadAndReaddirWire(t *testing.T) {
read := AppendRead(nil, AllZero, 512, 4096)
// The arguments follow the operation number.
d := xdr.NewDecoder(read[4:])
var st Stateid
raw, err := d.Raw(16)
if err != nil {
t.Fatalf("stateid: %v", err)
}
copy(st[:], raw)
off, err := d.Uint64()
count, err := d.Uint32()
if err != nil || off != 512 || count != 4096 {
t.Fatalf("read args: off %d count %d, %v", off, count, err)
}
res := AppendReadRes(nil, true, []byte("chunk"))
eof, data, err := DecodeReadBody(res)
if err != nil || !eof || string(data) != "chunk" {
t.Fatalf("read res: %v %q, %v", eof, data, err)
}
entries := []DirEntryRes{
{Cookie: 1, Name: "one", Attrs: Attrs{Type: NF4Reg}},
{Cookie: 2, Name: "two", Attrs: Attrs{Type: NF4Dir}},
}
req := OfBits(AttrType)
dir := AppendReadDirRes(nil, [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, entries, req, true)
verf, got, eof, err := DecodeReadDirBody(dir)
if err != nil || !eof {
t.Fatalf("readdir: eof %v, %v", eof, err)
}
if verf != [8]byte{1, 2, 3, 4, 5, 6, 7, 8} {
t.Fatalf("verifier %x", verf)
}
if len(got) != 2 || got[0].Name != "one" || got[0].Cookie != 1 ||
got[1].Attrs.Type != NF4Dir {
t.Fatalf("entries %+v", got)
}
}
func TestWriteAndCreateTruncated(t *testing.T) {
full := AppendWriteArgs(nil, AllZero, 8, StableFileSync, []byte("abc"))
// The args without the data length fail: the data is length prefixed
// and the cut lands inside it.
if _, _, _, _, err := DecodeWriteArgs(full[4 : len(full)-1]); err == nil {
t.Fatal("a truncated write body decoded")
}
if _, _, _, _, err := DecodeWriteArgs(full[4 : 4+16+8+4]); err == nil {
t.Fatal("write args without data decoded")
}
// A CREATE whose attribute list is cut short fails.
cre := AppendCreateArgs(nil, NF4Fifo, "p", "", 0, 0, 0o644)
if _, _, _, _, _, _, err := DecodeCreateArgs(cre[4 : len(cre)-1]); err == nil {
t.Fatal("a truncated attribute list decoded")
}
}
func TestNfsTimeOf(t *testing.T) {
if got := NfsTimeOf(5, 6); got != (NfsTime{5, 6}) {
t.Fatalf("NfsTimeOf: %+v", got)
}
}
func TestRemoveAndRenameWire(t *testing.T) {
remove := AppendRemoveArgs(nil, "victim")
if op, err := xdr.NewDecoder(remove).Uint32(); err != nil || op != OpRemove {
t.Fatalf("remove op: %d, %v", op, err)
}
if name, err := xdr.NewDecoder(remove[4:]).String(); err != nil || name != "victim" {
t.Fatalf("remove name: %q, %v", name, err)
}
rename := AppendRenameArgs(nil, "old", "new")
d := xdr.NewDecoder(rename)
if op, err := d.Uint32(); err != nil || op != OpRename {
t.Fatalf("rename op: %d, %v", op, err)
}
oldName, err := d.String()
newName, err := d.String()
if err != nil || oldName != "old" || newName != "new" {
t.Fatalf("rename names: %q %q, %v", oldName, newName, err)
}
// The change info is twenty bytes, the rename result carries two of
// them, and the measurement walks the wire forms.
if n, err := resBodyLen(OpRemove, ErrOK, AppendRemoveRes(nil)); err != nil || n != 20 {
t.Fatalf("remove measure: %d, %v", n, err)
}
renameRes := AppendRenameRes(nil)
if n, err := resBodyLen(OpRename, ErrOK, renameRes); err != nil || n != len(renameRes) {
t.Fatalf("rename measure: %d of %d, %v", n, len(renameRes), err)
}
}
func TestDecodeSetattrBlob(t *testing.T) {
// mode, size, both times, owner and group in one walk.
request := OfBits(AttrMode, AttrSize, AttrTimeAccessSet, AttrTimeModifySet,
AttrOwner, AttrOwnerGroup)
// The values walk the blob in ascending attribute order.
var blob []byte
blob = xdr.AppendUint64(blob, 4096) // size
blob = xdr.AppendUint32(blob, 0o640) // mode
blob = xdr.AppendString(blob, "1000@EXAMPLE") // owner
blob = xdr.AppendString(blob, "100@EXAMPLE") // owner group
blob = xdr.AppendBool(blob, false) // access: explicit time
blob = xdr.AppendInt64(blob, 100)
blob = xdr.AppendUint32(blob, 5)
blob = xdr.AppendBool(blob, true) // modify: server time
u, err := DecodeSetattrBlob(blob, request)
if err != nil {
t.Fatalf("decode: %v", err)
}
if !u.HasMode || u.Mode != 0o640 || !u.HasSize || u.Size != 4096 {
t.Fatalf("mode and size: %+v", u)
}
if u.Atime == nil || u.Atime.Server || u.Atime.Time != (NfsTime{100, 5}) {
t.Fatalf("atime: %+v", u.Atime)
}
if u.Mtime == nil || !u.Mtime.Server {
t.Fatalf("mtime: %+v", u.Mtime)
}
if u.UID == nil || *u.UID != 1000 || u.GID == nil || *u.GID != 100 {
t.Fatalf("owner: uid %v gid %v", u.UID, u.GID)
}
// A non numeric owner is refused, not silently dropped.
blob = xdr.AppendString(OfBits(AttrOwner).AppendTo(nil), "petr@EXAMPLE")
if _, err := DecodeSetattrBlob(blob, OfBits(AttrOwner)); err == nil {
t.Fatal("a non numeric owner decoded")
}
// An attribute the server cannot set stops the walk.
blob = append(xdr.AppendUint32(nil, 0), xdr.AppendUint32(nil, 0)...)
if _, err := DecodeSetattrBlob(blob, OfBits(AttrRawDev)); !errors.Is(err, ErrAttrNotSettable) {
t.Fatalf("rawdev: %v", err)
}
}
func TestSetattrAndLinkWire(t *testing.T) {
args := AppendSetattrArgs(nil, AllZero, OfBits(AttrMode), Attrs{Mode: 0o600})
// stateid first, then the fattr4: bitmap then length prefixed blob.
d := xdr.NewDecoder(args[4:])
if _, err := d.Raw(16); err != nil {
t.Fatalf("stateid: %v", err)
}
if _, err := ReadBitmap(d); err != nil {
t.Fatalf("bitmap: %v", err)
}
if _, err := d.VarOpaque(); err != nil {
t.Fatalf("blob: %v", err)
}
res := AppendSetattrRes(nil, OfBits(AttrMode, AttrSize))
attrsset, err := ReadBitmap(xdr.NewDecoder(res))
if err != nil || !attrsset.Has(AttrMode) || !attrsset.Has(AttrSize) {
t.Fatalf("attrsset: %v, %v", attrsset.Words(), err)
}
if n, err := resBodyLen(OpSetattr, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("setattr measure: %d of %d, %v", n, len(res), err)
}
link := AppendLinkArgs(nil, "newname")
if op, err := xdr.NewDecoder(link).Uint32(); err != nil || op != OpLink {
t.Fatalf("link op: %d, %v", op, err)
}
if name, err := xdr.NewDecoder(link[4:]).String(); err != nil || name != "newname" {
t.Fatalf("link name: %q, %v", name, err)
}
linkRes := AppendLinkRes(nil)
if n, err := resBodyLen(OpLink, ErrOK, linkRes); err != nil || n != len(linkRes) {
t.Fatalf("link measure: %d of %d, %v", n, len(linkRes), err)
}
}
func TestSetattrUpdatesWire(t *testing.T) {
uid, gid := uint32(1000), uint32(100)
u := SetAttrUpdates{
HasMode: true, Mode: 0o640,
HasSize: true, Size: 8192,
UID: &uid,
GID: &gid,
Atime: &NfsTimeSet{Server: true},
Mtime: &NfsTimeSet{Time: NfsTime{1_000_000_000, 7}},
}
arg := AppendSetattrArgsUpdates(nil, AllZero, u)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpSetattr {
t.Fatalf("op: %d, %v", op, err)
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, AllZero[:]) {
t.Fatalf("stateid: %x, %v", raw, err)
}
request, err := ReadBitmap(d)
blob, err := d.VarOpaque()
if err != nil {
t.Fatalf("fattr: %v", err)
}
for _, want := range []uint32{AttrMode, AttrSize, AttrOwner, AttrOwnerGroup,
AttrTimeAccessSet, AttrTimeModifySet} {
if !request.Has(want) {
t.Fatalf("the bitmap misses attribute %d", want)
}
}
got, err := DecodeSetattrBlob(blob, request)
if err != nil {
t.Fatalf("decode: %v", err)
}
if !got.HasMode || got.Mode != u.Mode || !got.HasSize || got.Size != u.Size {
t.Fatalf("mode and size: %+v", got)
}
if got.UID == nil || *got.UID != uid || got.GID == nil || *got.GID != gid {
t.Fatalf("owner: %v %v", got.UID, got.GID)
}
if got.Atime == nil || !got.Atime.Server {
t.Fatalf("atime: %+v", got.Atime)
}
if got.Mtime == nil || got.Mtime.Server || got.Mtime.Time != u.Mtime.Time {
t.Fatalf("mtime: %+v", got.Mtime)
}
}
func TestReadlinkAndCommitWire(t *testing.T) {
link := AppendReadlinkArgs(nil)
if op, err := xdr.NewDecoder(link).Uint32(); err != nil || op != OpReadlink {
t.Fatalf("readlink op: %d, %v", op, err)
}
res := AppendReadlinkRes(nil, "target.txt")
got, err := xdr.NewDecoder(res).String()
if err != nil || got != "target.txt" {
t.Fatalf("readlink res: %q, %v", got, err)
}
if n, err := resBodyLen(OpReadlink, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("readlink measure: %d of %d, %v", n, len(res), err)
}
commit := AppendCommitArgs(nil, 512, 1024, [8]byte{7, 7, 7, 7, 7, 7, 7, 7})
d := xdr.NewDecoder(commit)
if op, err := d.Uint32(); err != nil || op != OpCommit {
t.Fatalf("commit op: %d, %v", op, err)
}
d = xdr.NewDecoder(commit[4:])
off, err := d.Uint64()
count, err := d.Uint32()
verf, err := d.Raw(8)
if err != nil || off != 512 || count != 1024 || verf[0] != 7 {
t.Fatalf("commit args: %d %d %x, %v", off, count, verf, err)
}
commitRes := AppendCommitRes(nil, [8]byte{1, 2, 3, 4, 5, 6, 7, 8})
if n, err := resBodyLen(OpCommit, ErrOK, commitRes); err != nil || n != 8 {
t.Fatalf("commit measure: %d, %v", n, err)
}
gotVerf, err := xdr.NewDecoder(commitRes).Raw(8)
if err != nil || gotVerf[7] != 8 {
t.Fatalf("commit res: %x, %v", gotVerf, err)
}
}
func TestSecinfoWire(t *testing.T) {
args := AppendSecinfoArgs(nil, "a.txt")
if op, err := xdr.NewDecoder(args).Uint32(); err != nil || op != OpSecinfo {
t.Fatalf("secinfo op: %d, %v", op, err)
}
if name, err := xdr.NewDecoder(args[4:]).String(); err != nil || name != "a.txt" {
t.Fatalf("secinfo name: %q, %v", name, err)
}
// The no name argument is the style enum alone: four bytes and
// nothing after them, RFC 7863.
noname := AppendSecinfoNoNameArgs(nil, StyleCurrentFH)
if len(noname) != 8 {
t.Fatalf("no name args of %d bytes, want 8", len(noname))
}
d := xdr.NewDecoder(noname)
if op, err := d.Uint32(); err != nil || op != OpSecinfoNoName {
t.Fatalf("no name op: %d, %v", op, err)
}
style, err := DecodeSecinfoNoNameArgs(d)
if err != nil || style != StyleCurrentFH || d.Remaining() != 0 {
t.Fatalf("no name style %d, %v", style, err)
}
if _, err := DecodeSecinfoNoNameArgs(xdr.NewDecoder(xdr.AppendUint32(nil, 5))); err == nil {
t.Fatal("an unknown style accepted")
}
// The result: one word per flavour, the rpcsec_gss_info body only on
// the RPCSEC_GSS branch, RFC 7863.
res := AppendSecinfoRes(nil, []SecinfoEntry{{Flavor: SecFlavorSys}})
if len(res) != 8 {
t.Fatalf("auth_sys entry of %d bytes, want 8", len(res))
}
entries, err := DecodeSecinfoRes(res)
if err != nil || len(entries) != 1 || entries[0].Flavor != SecFlavorSys ||
entries[0].GSS != nil {
t.Fatalf("secinfo res: %+v, %v", entries, err)
}
if n, err := resBodyLen(OpSecinfo, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("secinfo measure: %d of %d, %v", n, len(res), err)
}
gssRes := AppendSecinfoRes(nil, []SecinfoEntry{
{Flavor: SecFlavorSys},
{Flavor: SecFlavorGSS, GSS: &GSSInfo{OID: []byte{0x2a}, QOP: 0, Service: 1}},
})
entries, err = DecodeSecinfoRes(gssRes)
if err != nil || len(entries) != 2 || entries[1].GSS == nil ||
len(entries[1].GSS.OID) != 1 || entries[1].GSS.Service != 1 {
t.Fatalf("gss entry: %+v, %v", entries, err)
}
if n, err := resBodyLen(OpSecinfoNoName, ErrOK, gssRes); err != nil || n != len(gssRes) {
t.Fatalf("no name measure: %d of %d, %v", n, len(gssRes), err)
}
}
func TestDecodeLocktDeniedCompound(t *testing.T) {
var ops [][]byte
ops = append(ops, append(AppendOpHeader(nil, OpSequence, ErrOK), make([]byte, 36)...))
ops = append(ops, AppendOpHeader(nil, OpPutRootfh, ErrOK))
deniedBody := AppendLocktResDenied(AppendOpHeader(nil, OpLockt, ErrDenied),
0, 100, LockTypeWrite, 0x1111, []byte("locker-a"))
ops = append(ops, deniedBody)
payload := AppendCompoundRes(nil, ErrDenied, "t", ops)
res, bodies, err := DecodeCompoundResBodies(payload)
if err != nil {
t.Fatalf("decode: %v", err)
}
if res.Status != ErrDenied || len(res.Ops) != 3 {
t.Fatalf("res: status %d ops %d", res.Status, len(res.Ops))
}
for i, op := range res.Ops {
t.Logf("op %d: op=%d status=%d bodylen=%d", i, op.Op, op.Status, len(bodies[i]))
}
if len(bodies[2]) == 0 {
t.Fatal("the denied body is empty")
}
got, err := DecodeLocktResBody(res.Status, bodies[2])
if err != nil {
t.Fatalf("denied decode: %v", err)
}
if got.ClientID != 0x1111 || got.LockType != LockTypeWrite || got.Length != 100 {
t.Fatalf("denied: %+v", got)
}
}
func TestLockWire(t *testing.T) {
openSt := AllZero
arg := AppendLockArgsNew(nil, openSt, 0x1111, []byte("owner"), LockTypeWrite, false, 8, 16)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpLock {
t.Fatalf("op: %d, %v", op, err)
}
if lt, err := d.Uint32(); err != nil || lt != LockTypeWrite {
t.Fatalf("locktype: %v", err)
}
if reclaim, err := d.Bool(); err != nil || reclaim {
t.Fatal("reclaim set")
}
if off, err := d.Uint64(); err != nil || off != 8 {
t.Fatalf("offset: %v", err)
}
if length, err := d.Uint64(); err != nil || length != 16 {
t.Fatalf("length: %v", err)
}
if newOwner, err := d.Bool(); err != nil || !newOwner {
t.Fatal("new owner flag missing")
}
if _, err := d.Uint32(); err != nil { // open seqid
t.Fatal(err)
}
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, openSt[:]) {
t.Fatal("open stateid mismatch")
}
if _, err := d.Uint32(); err != nil { // lock seqid
t.Fatal(err)
}
if clientid, err := d.Uint64(); err != nil || clientid != 0x1111 {
t.Fatalf("clientid: %v", err)
}
if owner, err := d.VarOpaque(); err != nil || string(owner) != "owner" {
t.Fatalf("owner: %q, %v", owner, err)
}
existing := AppendLockArgsExisting(nil, AllZero, LockTypeRead, 0, 32)
d = xdr.NewDecoder(existing[4:])
if _, err := d.Uint32(); err != nil { // lock type
t.Fatal(err)
}
if reclaim, err := d.Bool(); err != nil || reclaim {
t.Fatal("existing lock marked as reclaim")
}
if off, err := d.Uint64(); err != nil || off != 0 {
t.Fatalf("offset: %v", err)
}
if length, err := d.Uint64(); err != nil || length != 32 {
t.Fatalf("length: %v", err)
}
if newOwner, err := d.Bool(); err != nil || newOwner {
t.Fatal("existing lock carried the new owner form")
}
if _, err := d.Raw(16); err != nil {
t.Fatal(err)
}
if _, err := d.Uint32(); err != nil { // lock seqid
t.Fatal(err)
}
var lockSt Stateid
lockSt[4], lockSt[5], lockSt[6], lockSt[7] = 'L', 'O', 'C', 'K'
lockRes := AppendLockRes(nil, lockSt)
if n, err := resBodyLen(OpLock, ErrOK, lockRes); err != nil || n != 16 {
t.Fatalf("lock measure: %d, %v", n, err)
}
lockt := AppendLocktArgs(nil, LockTypeRead, 0, 64, 0x2222, []byte("prober"))
d = xdr.NewDecoder(lockt[4:])
if lt, err := d.Uint32(); err != nil || lt != LockTypeRead {
t.Fatalf("lockt type: %v", err)
}
if _, err := d.Uint64(); err != nil {
t.Fatal(err)
}
if _, err := d.Uint64(); err != nil {
t.Fatal(err)
}
if cid, err := d.Uint64(); err != nil || cid != 0x2222 {
t.Fatalf("lockt clientid: %v", err)
}
if owner, err := d.VarOpaque(); err != nil || string(owner) != "prober" {
t.Fatalf("lockt owner: %q, %v", owner, err)
}
denied := AppendLocktResDenied(nil, 0, 100, LockTypeWrite, 0x3333, []byte("holder"))
got, err := DecodeLocktResBody(ErrDenied, denied)
if err != nil || got.Offset != 0 || got.Length != 100 ||
got.LockType != LockTypeWrite || got.ClientID != 0x3333 ||
string(got.Owner) != "holder" {
t.Fatalf("denied: %+v, %v", got, err)
}
if n, err := resBodyLenLockt(ErrDenied, denied); err != nil || n != len(denied) {
t.Fatalf("lockt measure: %d of %d, %v", n, len(denied), err)
}
if _, err := resBodyLenLockt(ErrDenied, []byte{0, 0, 0, 0, 0, 0, 0, 0}); err == nil {
t.Fatal("a truncated denied body measured without error")
}
}