// Copyright (c) 2026 Petr BalvĂ­n (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") } }