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

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-21 18:51:17 +02:00
commit a9b8039ef7
153 changed files with 34403 additions and 0 deletions
+243
View File
@@ -0,0 +1,243 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"slices"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Attribute numbers of the fattr4 table, RFC 8881 section 5. The numbers
// were verified against the XDR description of the standard rather than
// recalled: an off by one here shows up as garbage on the client.
const (
AttrSupportedAttrs = 0 // bitmap4
AttrType = 1 // enum
AttrFHExpireType = 2 // uint32
AttrChange = 3 // uint64
AttrSize = 4 // uint64
AttrLinkSupport = 5 // bool
AttrSymlinkSupport = 6 // bool
AttrNamedAttr = 7 // bool
AttrFSID = 8 // uint64, uint64
AttrUniqueHandles = 9 // bool
AttrLeaseTime = 10 // uint32
AttrRdattrError = 11 // enum
AttrFileHandle = 19 // opaque
AttrFileID = 20 // uint64
AttrMaxName = 29 // uint32
AttrMaxRead = 30 // uint64
AttrMaxWrite = 31 // uint64
AttrMode = 33 // uint32
AttrNumlinks = 35 // uint32
AttrOwner = 36 // string
AttrOwnerGroup = 37 // string
AttrRawDev = 41 // specdata
AttrSpaceUsed = 45 // uint64
AttrTimeAccessSet = 48 // settime4
AttrTimeModifySet = 54 // settime4
AttrTimeAccess = 47 // nfstime4
AttrTimeMetadata = 52 // nfstime4
AttrTimeModify = 53 // nfstime4
AttrMountedOnFileID = 55 // uint64
AttrSuppattrExclCreat = 75 // bitmap4
)
// SupportedAttrs is the attribute set this server advertises. A client asks
// only for attributes inside the set it read from here.
var SupportedAttrs = OfBits(
AttrSupportedAttrs,
AttrType,
AttrFHExpireType,
AttrChange,
AttrSize,
AttrLinkSupport,
AttrSymlinkSupport,
AttrNamedAttr,
AttrFSID,
AttrUniqueHandles,
AttrFileHandle,
AttrFileID,
AttrMaxName,
AttrMaxRead,
AttrMaxWrite,
AttrMode,
AttrNumlinks,
AttrOwner,
AttrOwnerGroup,
AttrSpaceUsed,
AttrTimeAccess,
AttrTimeMetadata,
AttrTimeModify,
AttrMountedOnFileID,
)
// ServerLimits carries the per export constants the advertised attributes
// report back.
type ServerLimits struct {
MaxName uint32
MaxRead uint64
MaxWrite uint64
}
// DefaultLimits are the limits the server advertises.
var DefaultLimits = ServerLimits{MaxName: 255, MaxRead: 1 << 22, MaxWrite: 1 << 22}
// Attrs holds the values the fattr4 builder emits. A zero field for an
// attribute the server does not have is simply not requested by a client
// that honoured the advertised set.
type Attrs struct {
Type uint32
FHExpireType uint32
Change uint64
Size uint64
LinkSupport bool
SymlinkSupport bool
NamedAttr bool
FSID [2]uint64
UniqueHandles bool
FileHandle []byte
FileID uint64
Mode uint32
Numlinks uint32
Owner string // decimal uid, RFC 8881 section 5.8
OwnerGroup string // decimal gid
SpaceUsed uint64
TimeAccess NfsTime
TimeMetadata NfsTime
TimeModify NfsTime
MountedOnFileID uint64
Limits ServerLimits
// FsLocations and FsLocationsInfo carry the pre encoded attributes
// for referral stubs; nil omits them.
FsLocations []byte
FsLocationsInfo []byte
}
// A NfsTime is an nfstime4: seconds and nanoseconds since the epoch.
type NfsTime struct {
Seconds int64
Nseconds uint32
}
// NfsTimeOf converts a Go time.
func NfsTimeOf(sec int64, nsec uint32) NfsTime { return NfsTime{sec, nsec} }
func appendNfsTime(b []byte, t NfsTime) []byte {
b = xdr.AppendInt64(b, t.Seconds)
return xdr.AppendUint32(b, t.Nseconds)
}
// AppendFattr emits an fattr4 for the requested attributes: the request
// bitmap filtered to what this server has, then the length prefixed list of
// values in numeric order, as the encoding requires.
func AppendFattr(b []byte, request Bitmap, a Attrs) []byte {
var body []byte
response := Bitmap{}
for _, n := range sortedBits(request) {
switch n {
case AttrSupportedAttrs:
body = SupportedAttrs.AppendTo(body)
response = response.With(n)
case AttrType:
body = xdr.AppendUint32(body, a.Type)
response = response.With(n)
case AttrFHExpireType:
body = xdr.AppendUint32(body, a.FHExpireType)
response = response.With(n)
case AttrChange:
body = xdr.AppendUint64(body, a.Change)
response = response.With(n)
case AttrSize:
body = xdr.AppendUint64(body, a.Size)
response = response.With(n)
case AttrLinkSupport:
body = xdr.AppendBool(body, a.LinkSupport)
response = response.With(n)
case AttrSymlinkSupport:
body = xdr.AppendBool(body, a.SymlinkSupport)
response = response.With(n)
case AttrNamedAttr:
body = xdr.AppendBool(body, a.NamedAttr)
response = response.With(n)
case AttrFSID:
body = xdr.AppendUint64(body, a.FSID[0])
body = xdr.AppendUint64(body, a.FSID[1])
response = response.With(n)
case AttrUniqueHandles:
body = xdr.AppendBool(body, a.UniqueHandles)
response = response.With(n)
case AttrFileHandle:
body = xdr.AppendVarOpaque(body, a.FileHandle)
response = response.With(n)
case AttrFileID:
body = xdr.AppendUint64(body, a.FileID)
response = response.With(n)
case AttrMaxName:
body = xdr.AppendUint32(body, a.Limits.MaxName)
response = response.With(n)
case AttrMaxRead:
body = xdr.AppendUint64(body, a.Limits.MaxRead)
response = response.With(n)
case AttrMaxWrite:
body = xdr.AppendUint64(body, a.Limits.MaxWrite)
response = response.With(n)
case AttrMode:
body = xdr.AppendUint32(body, a.Mode)
response = response.With(n)
case AttrNumlinks:
body = xdr.AppendUint32(body, a.Numlinks)
response = response.With(n)
case AttrOwner:
body = xdr.AppendString(body, a.Owner)
response = response.With(n)
case AttrOwnerGroup:
body = xdr.AppendString(body, a.OwnerGroup)
response = response.With(n)
case AttrSpaceUsed:
body = xdr.AppendUint64(body, a.SpaceUsed)
response = response.With(n)
case AttrTimeAccess:
body = appendNfsTime(body, a.TimeAccess)
response = response.With(n)
case AttrTimeMetadata:
body = appendNfsTime(body, a.TimeMetadata)
response = response.With(n)
case AttrTimeModify:
body = appendNfsTime(body, a.TimeModify)
response = response.With(n)
case AttrMountedOnFileID:
body = xdr.AppendUint64(body, a.MountedOnFileID)
response = response.With(n)
case AttrFsLocations:
if a.FsLocations != nil {
body = append(body, a.FsLocations...)
response = response.With(n)
}
case AttrFsLocationsInfo:
if a.FsLocationsInfo != nil {
body = append(body, a.FsLocationsInfo...)
response = response.With(n)
}
}
}
b = response.AppendTo(b)
return xdr.AppendVarOpaque(b, body)
}
// sortedBits lists the attribute numbers set in the bitmap, ascending.
func sortedBits(m Bitmap) []uint32 {
var out []uint32
for w, word := range m.Words() {
for bit := range 32 {
if word&(1<<bit) != 0 {
out = append(out, uint32(w*32+bit))
}
}
}
slices.Sort(out)
return out
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"slices"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// maxBitmapWords is the word count limit of a bitmap4. The protocol caps
// the array at 100 words; the attribute table today ends far below that.
const maxBitmapWords = 100
// A Bitmap is a bitmap4: a compact set of attribute numbers.
type Bitmap []uint32
// OfBits returns a Bitmap carrying exactly the given attribute numbers.
func OfBits(bits ...uint32) Bitmap {
if len(bits) == 0 {
return Bitmap{}
}
var words Bitmap
for _, n := range bits {
w := int(n / 32)
for len(words) <= w {
words = append(words, 0)
}
words[w] |= 1 << (n % 32)
}
return words
}
// Has reports whether the attribute number n is set.
func (m Bitmap) Has(n uint32) bool {
w := int(n / 32)
return w < len(m) && m[w]&(1<<(n%32)) != 0
}
// With returns a bitmap carrying n in addition to what was already set.
// Words the original bitmap already held are never dropped, even when the
// new bit lands in a lower word.
func (m Bitmap) With(n uint32) Bitmap {
w := int(n / 32)
out := make(Bitmap, max(w+1, len(m)))
copy(out, m)
out[w] |= 1 << (n % 32)
return out
}
// AppendTo appends the bitmap in wire form: a count of words followed by
// the words, with trailing zero words trimmed.
func (m Bitmap) AppendTo(b []byte) []byte {
trimmed := m
for len(trimmed) > 0 && trimmed[len(trimmed)-1] == 0 {
trimmed = trimmed[:len(trimmed)-1]
}
b = xdr.AppendUint32(b, uint32(len(trimmed)))
for _, w := range trimmed {
b = xdr.AppendUint32(b, w)
}
return b
}
// ReadBitmap reads a bitmap4. A bitmap beyond the word limit is refused
// before any of it is buffered.
func ReadBitmap(d *xdr.Decoder) (Bitmap, error) {
n, err := d.Uint32()
if err != nil {
return nil, err
}
if n > maxBitmapWords {
return nil, xdr.ErrBadLength
}
m := make(Bitmap, 0, n)
for range n {
w, err := d.Uint32()
if err != nil {
return nil, err
}
m = append(m, w)
}
return m, nil
}
// Words reports the words of the bitmap, without trailing zero words.
func (m Bitmap) Words() []uint32 {
trimmed := slices.Clone(m)
for len(trimmed) > 0 && trimmed[len(trimmed)-1] == 0 {
trimmed = trimmed[:len(trimmed)-1]
}
return trimmed
}
+205
View File
@@ -0,0 +1,205 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The callback channel of RFC 8881: the CB_COMPOUND procedure the server
// calls on the connection the client opened, CB_SEQUENCE driving the back
// channel slot table, and CB_RECALL recalling a delegation.
package nfs4
import (
"fmt"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Callback operation numbers, RFC 8881 section 15.5.
const (
OpCBGetattr = 3
OpCBRecall = 4
OpCBSequence = 11
OpCBPushDeleg = 7
OpCBNotify = 6
OpCBNotifyLock = 13
)
// The callback program rides on the same minor version and the COMPOUND
// shape as the fore channel.
const (
CBDefaultProgram = 0x40000000
)
// AppendCBCompoundArgs encodes the CB_COMPOUND4args: the tag, the minor
// version, the callback identifier and the operations. The callback
// identifier is the slot the server reserved for this client when the
// back channel was negotiated; with sessions it stays zero.
func AppendCBCompoundArgs(b []byte, tag string, minorversion, callbackIdent uint32, ops [][]byte) []byte {
b = xdr.AppendString(b, tag)
b = xdr.AppendUint32(b, minorversion)
b = xdr.AppendUint32(b, callbackIdent)
b = xdr.AppendUint32(b, uint32(len(ops)))
for _, op := range ops {
b = append(b, op...)
}
return b
}
// CBCompoundHeader is the head of a decoded CB_COMPOUND4args.
type CBCompoundHeader struct {
Tag string
Minor uint32
CallbackIdent uint32
OpCount uint32
}
// DecodeCBCompoundArgs splits CB_COMPOUND4args into its header and a
// decoder positioned at the first argop.
func DecodeCBCompoundArgs(payload []byte) (CBCompoundHeader, *xdr.Decoder, error) {
d := xdr.NewDecoder(payload)
var h CBCompoundHeader
var err error
if h.Tag, err = d.String(); err != nil {
return h, nil, err
}
if h.Minor, err = d.Uint32(); err != nil {
return h, nil, err
}
if h.CallbackIdent, err = d.Uint32(); err != nil {
return h, nil, err
}
if h.OpCount, err = d.Uint32(); err != nil {
return h, nil, err
}
if h.OpCount > maxOps {
return h, nil, fmt.Errorf("nfs4: %d operations exceeds %d", h.OpCount, maxOps)
}
return h, d, nil
}
// AppendCBCompoundRes encodes a CB_COMPOUND4res: the top level status, the
// echoed tag and the result array.
func AppendCBCompoundRes(b []byte, status uint32, tag string, results [][]byte) []byte {
return AppendCompoundRes(b, status, tag, results)
}
// AppendCBSequenceArgs encodes the CB_SEQUENCE argop.
func AppendCBSequenceArgs(b []byte, id SessionID, sequence, slot, highestSlot uint32, cacheThis bool) []byte {
b = xdr.AppendUint32(b, OpCBSequence)
return AppendSequenceArgsTail(b, id, sequence, slot, highestSlot, cacheThis)
}
// AppendSequenceArgsTail appends the shared SEQUENCE argument fields: the
// session id, the sequence, the slot ids and the caching flag.
func AppendSequenceArgsTail(b []byte, id SessionID, sequence, slot, highestSlot uint32, cacheThis bool) []byte {
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, slot)
b = xdr.AppendUint32(b, highestSlot)
return xdr.AppendBool(b, cacheThis)
}
// A CBSequenceArgs is the decoded CB_SEQUENCE4args.
type CBSequenceArgs struct {
SessionID SessionID
Sequence uint32
Slot uint32
HighestSlot uint32
CacheThis bool
}
// DecodeCBSequenceArgs decodes the CB_SEQUENCE4args from the decoder,
// which is positioned after the operation number.
func DecodeCBSequenceArgs(d *xdr.Decoder) (CBSequenceArgs, error) {
var a CBSequenceArgs
raw, err := d.Raw(16)
if err != nil {
return a, err
}
copy(a.SessionID[:], raw)
var err2 error
if a.Sequence, err2 = d.Uint32(); err2 != nil {
return a, err2
}
if a.Slot, err2 = d.Uint32(); err2 != nil {
return a, err2
}
if a.HighestSlot, err2 = d.Uint32(); err2 != nil {
return a, err2
}
if a.CacheThis, err2 = d.Bool(); err2 != nil {
return a, err2
}
return a, nil
}
// AppendCBSequenceRes encodes the successful CB_SEQUENCE4resok of
// RFC 7863: the session id, the echoed identifiers and the target
// highest slot. No further word follows.
func AppendCBSequenceRes(b []byte, id SessionID, sequence, slot, highestSlot uint32) []byte {
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, slot)
b = xdr.AppendUint32(b, highestSlot)
return xdr.AppendUint32(b, highestSlot) // cbsr_target_highest_slotid
}
// DecodeCBSequenceRes decodes the successful CB_SEQUENCE4resok.
func DecodeCBSequenceRes(d *xdr.Decoder) (id SessionID, sequence, slot, highestSlot, targetHighestSlot uint32, err error) {
raw, rerr := d.Raw(16)
if rerr != nil {
return id, 0, 0, 0, 0, rerr
}
copy(id[:], raw)
if sequence, err = d.Uint32(); err != nil {
return
}
if slot, err = d.Uint32(); err != nil {
return
}
if highestSlot, err = d.Uint32(); err != nil {
return
}
targetHighestSlot, err = d.Uint32()
return
}
// AppendCBRecallArgs encodes the CB_RECALL argop: the delegation stateid,
// whether to truncate, and the file handle the delegation names.
func AppendCBRecallArgs(b []byte, stateid Stateid, truncate bool, fh []byte) []byte {
b = xdr.AppendUint32(b, OpCBRecall)
b = stateid.Append(b)
b = xdr.AppendBool(b, truncate)
return xdr.AppendVarOpaque(b, fh)
}
// A CBRecallArgs is the decoded CB_RECALL argop.
type CBRecallArgs struct {
Stateid Stateid
Truncate bool
FH []byte
}
// DecodeCBRecallArgs decodes the CB_RECALL args from the decoder, which is
// positioned after the operation number.
func DecodeCBRecallArgs(d *xdr.Decoder) (CBRecallArgs, error) {
var a CBRecallArgs
raw, err := d.Raw(16)
if err != nil {
return a, err
}
copy(a.Stateid[:], raw)
var err2 error
if a.Truncate, err2 = d.Bool(); err2 != nil {
return a, err2
}
a.FH, err2 = d.VarOpaque()
return a, err2
}
// AppendCBGetattrArgs encodes the CB_GETATTR argop: the handle and the
// requested attribute bitmap.
func AppendCBGetattrArgs(b []byte, fh []byte, request Bitmap) []byte {
b = xdr.AppendUint32(b, OpCBGetattr)
b = xdr.AppendVarOpaque(b, fh)
return request.AppendTo(b)
}
+251
View File
@@ -0,0 +1,251 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestCBCompoundRoundTrip(t *testing.T) {
ops := [][]byte{
AppendCBSequenceArgs(nil, MakeSessionID([8]byte{}, 5), 1, 0, 0, true),
AppendCBRecallArgs(nil, AllZero, false, []byte{9, 9}),
}
arg := AppendCBCompoundArgs(nil, "cb", 2, 0, ops)
h, d, err := DecodeCBCompoundArgs(arg)
if err != nil {
t.Fatalf("decode: %v", err)
}
if h.Tag != "cb" || h.Minor != 2 || h.CallbackIdent != 0 || h.OpCount != 2 {
t.Fatalf("header: %+v", h)
}
if op, err := d.Uint32(); err != nil || op != OpCBSequence {
t.Fatalf("op 0: %d, %v", op, err)
}
a, err := DecodeCBSequenceArgs(d)
if err != nil {
t.Fatalf("cb sequence: %v", err)
}
if a.Sequence != 1 || a.Slot != 0 || !a.CacheThis {
t.Fatalf("cb sequence args: %+v", a)
}
if op, err := d.Uint32(); err != nil || op != OpCBRecall {
t.Fatalf("op 1: %d, %v", op, err)
}
r, err := DecodeCBRecallArgs(d)
if err != nil || r.Truncate || !bytes.Equal(r.FH, []byte{9, 9}) {
t.Fatalf("cb recall args: %+v, %v", r, err)
}
}
func TestCBSequenceResShape(t *testing.T) {
id := MakeSessionID([8]byte{}, 0x55)
res := AppendCBSequenceRes(nil, id, 9, 2, 7)
if len(res) != 32 {
t.Fatalf("res of %d bytes, want 32", len(res))
}
d := xdr.NewDecoder(res)
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, id[:]) {
t.Fatalf("session id: %x, %v", raw, err)
}
for _, want := range []uint32{9, 2, 7, 7} {
if v, err := d.Uint32(); err != nil || v != want {
t.Fatalf("word: %d, %v; want %d", v, err, want)
}
}
sid, seq, slot, high, target, err := DecodeCBSequenceRes(xdr.NewDecoder(res))
if err != nil || sid != id || seq != 9 || slot != 2 || high != 7 || target != 7 {
t.Fatalf("decode: %d %d %d %d %d, %v", seq, slot, high, target, 0, err)
}
}
func TestCBGetattrArgs(t *testing.T) {
arg := AppendCBGetattrArgs(nil, []byte("fh"), OfBits(AttrSize, AttrChange))
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpCBGetattr {
t.Fatalf("op: %d, %v", op, err)
}
if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh" {
t.Fatalf("fh: %q, %v", fh, err)
}
if m, err := ReadBitmap(d); err != nil || !m.Has(AttrSize) || !m.Has(AttrChange) {
t.Fatalf("bitmap: %v", m.Words())
}
}
func TestDecodeCompoundResBodiesCB(t *testing.T) {
var results [][]byte
seqBody := append(AppendOpHeader(nil, OpCBSequence, ErrOK),
AppendCBSequenceRes(nil, MakeSessionID([8]byte{}, 3), 4, 0, 7)...)
results = append(results, seqBody, AppendOpHeader(nil, OpCBRecall, ErrOK))
payload := AppendCBCompoundRes(nil, ErrOK, "cbt", results)
res, bodies, err := DecodeCompoundResBodiesCB(payload)
if err != nil {
t.Fatalf("decode: %v", err)
}
if res.Status != ErrOK || len(res.Ops) != 2 {
t.Fatalf("res: status %d ops %d", res.Status, len(res.Ops))
}
if len(bodies[0]) != 32 || len(bodies[1]) != 0 {
t.Fatalf("bodies: %d and %d", len(bodies[0]), len(bodies[1]))
}
// A failed op carries no body.
payload = AppendCBCompoundRes(nil, ErrNotSupp, "", [][]byte{
AppendOpHeader(nil, OpCBGetattr, ErrNotSupp),
})
res, bodies, err = DecodeCompoundResBodiesCB(payload)
if err != nil || res.Status != ErrNotSupp || len(bodies[0]) != 0 {
t.Fatalf("failed op: status %d body %d, %v", res.Status, len(bodies[0]), err)
}
}
func TestDelegationCodecRoundTrip(t *testing.T) {
var st Stateid
st[0] = 3
st[4], st[5], st[6], st[7] = 'D', 'E', 'L', 'E'
// READ delegation round trip. The res body ends with the NONE type,
// which the READ delegation replaces.
base := AppendOpenRes(nil, AllZero)
body := AppendDelegation(base[:len(base)-4], OpenDelegRead, st, false, 0)
openSt, delegType, delegSt, err := DecodeOpenResDeleg(body)
if err != nil {
t.Fatalf("read decode: %v", err)
}
if delegType != OpenDelegRead || delegSt != st || openSt != AllZero {
t.Fatalf("read delegation: %+v", delegSt)
}
// WRITE delegation round trip with a space limit.
body = AppendDelegation(base[:len(base)-4], OpenDelegWrite, st, true, 1<<20)
openSt, delegType, delegSt, err = DecodeOpenResDeleg(body)
if err != nil {
t.Fatalf("write decode: %v", err)
}
if delegType != OpenDelegWrite || delegSt != st {
t.Fatalf("write delegation: %d, %v", delegType, err)
}
// NONE carries nothing beyond the type.
body = AppendDelegation(base[:len(base)-4], OpenDelegNone, st, false, 0)
_, delegType, _, derr := DecodeOpenResDeleg(body)
if derr != nil || delegType != OpenDelegNone {
t.Fatalf("none: %d, %v", delegType, derr)
}
if n, err := resBodyLenCB(OpCBSequence, ErrOK, append(AppendOpHeader(nil, OpCBSequence, ErrOK),
AppendCBSequenceRes(nil, MakeSessionID([8]byte{}, 1), 1, 0, 7)...)); err != nil || n != 32 {
t.Fatalf("cb seq measure: %d, %v", n, err)
}
}
func seqBodyHelper() []byte {
return append(AppendOpHeader(nil, OpCBSequence, ErrOK),
AppendCBSequenceRes(nil, MakeSessionID([8]byte{}, 1), 1, 0, 7)...)
}
func TestAppendOpenArgsPrevious(t *testing.T) {
arg := AppendOpenArgsPrevious(nil, 0x7777, []byte("owner"), ShareAccessBoth, 0)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpOpen {
t.Fatalf("op: %d, %v", op, err)
}
if seq, err := d.Uint32(); err != nil || seq != 0 {
t.Fatalf("seqid: %v", err)
}
if access, err := d.Uint32(); err != nil || access != ShareAccessBoth {
t.Fatalf("access: %v", err)
}
if deny, err := d.Uint32(); err != nil || deny != 0 {
t.Fatalf("deny: %v", err)
}
if cid, err := d.Uint64(); err != nil || cid != 0x7777 {
t.Fatalf("clientid: %v", err)
}
owner, err := d.VarOpaque()
if err != nil || string(owner) != "owner" {
t.Fatalf("owner: %q, %v", owner, err)
}
if ot, err := d.Uint32(); err != nil || ot != 0 {
t.Fatalf("opentype: %d, %v", ot, err)
}
if claim, err := d.Uint32(); err != nil || claim != ClaimPrevious {
t.Fatalf("claim: %d, %v", claim, err)
}
}
func TestAppendOpenArgsPreviousRoundTrip(t *testing.T) {
arg := AppendOpenArgsPrevious(nil, 0x7777, []byte("owner"), ShareAccessBoth, 0)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpOpen {
t.Fatalf("op: %d, %v", op, err)
}
if seq, err := d.Uint32(); err != nil || seq != 0 {
t.Fatalf("seqid: %v", err)
}
if access, err := d.Uint32(); err != nil || access != ShareAccessBoth {
t.Fatalf("access: %v", err)
}
if deny, err := d.Uint32(); err != nil || deny != 0 {
t.Fatalf("deny: %v", err)
}
if cid, err := d.Uint64(); err != nil || cid != 0x7777 {
t.Fatalf("clientid: %v", err)
}
owner, err := d.VarOpaque()
if err != nil || string(owner) != "owner" {
t.Fatalf("owner: %q, %v", owner, err)
}
if ot, err := d.Uint32(); err != nil || ot != 0 {
t.Fatalf("opentype: %d, %v", ot, err)
}
if claim, err := d.Uint32(); err != nil || claim != ClaimPrevious {
t.Fatalf("claim: %d, %v", claim, err)
}
}
func TestDecodeOpenResDelegTruncated(t *testing.T) {
// Every proper prefix of a READ delegation answer must fail to decode.
st := AllZero
st[0] = 3
base := AppendOpenRes(nil, AllZero)
base = base[:len(base)-4] // drop the NONE delegation type
full := append(append([]byte{}, base...), AppendDelegation(nil, OpenDelegRead, st, false, 0)...)
for n := 4; n < len(full); n++ {
if _, _, _, err := DecodeOpenResDeleg(full[:n]); err == nil {
t.Fatalf("a %d byte prefix decoded", n)
}
}
}
func TestDecodeSessionChannelRoundTrip(t *testing.T) {
// No RDMA: six words and an empty size array.
var body []byte
body = xdr.AppendUint32(body, 0)
body = xdr.AppendUint32(body, 1<<20)
body = xdr.AppendUint32(body, 1<<20)
body = xdr.AppendUint32(body, 1<<20)
body = xdr.AppendUint32(body, 8)
body = xdr.AppendUint32(body, 8)
body = xdr.AppendUint32(body, 0)
c, err := DecodeSessionChannel(xdr.NewDecoder(body))
if err != nil {
t.Fatalf("decode: %v", err)
}
if c.MaxRequests != 8 || c.RdmaIRDEnabled {
t.Fatalf("channel: %+v", c)
}
}
func TestDecodeExchangeIDArgsTruncated(t *testing.T) {
// AppendLockArgsNew writes the op, the open seqid, the open stateid,
// the lock seqid, the client id and the owner; the truncation test
// walks the payload after the op number.
full := AppendExchangeIDArgs(nil, [8]byte{1}, []byte("own"), 0)
for n := 1; n < len(full)-4; n++ {
if _, err := DecodeExchangeIDArgs(xdr.NewDecoder(full[4 : 4+n])); err == nil {
t.Fatalf("a %d byte prefix decoded", n)
}
}
}
+823
View File
@@ -0,0 +1,823 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"errors"
"fmt"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// maxOps bounds the COMPOUND arrays on both encode and decode. The
// protocol has no fixed limit; a request longer than this is not a client,
// it is an attack.
const maxOps = 1024
// maxTag bounds the COMPOUND tag.
const maxTag = 128
// AppendCompoundArgs encodes the COMPOUND4args: the tag, the minor version
// and the operation array, where each element is an encoded argop: its
// operation number followed by its own arguments.
func AppendCompoundArgs(b []byte, tag string, minor uint32, ops [][]byte) []byte {
b = xdr.AppendString(b, tag)
b = xdr.AppendUint32(b, minor)
b = xdr.AppendUint32(b, uint32(len(ops)))
for _, op := range ops {
b = append(b, op...)
}
return b
}
// CompoundHeader is the head of a decoded COMPOUND4args: the tag, the minor
// version and the operation count. The returned decoder is positioned at
// the first argop, and the dispatcher reads each operation's own arguments
// from it as it dispatches.
type CompoundHeader struct {
Tag string
Minor uint32
OpCount uint32
}
// DecodeCompoundArgs splits COMPOUND4args into its header and a decoder
// over the argops.
func DecodeCompoundArgs(payload []byte) (CompoundHeader, *xdr.Decoder, error) {
d := xdr.NewDecoder(payload)
var h CompoundHeader
var err error
if h.Tag, err = d.String(); err != nil {
return h, nil, err
}
if len(h.Tag) > maxTag {
return h, nil, fmt.Errorf("nfs4: tag of %d bytes exceeds %d", len(h.Tag), maxTag)
}
if h.Minor, err = d.Uint32(); err != nil {
return h, nil, err
}
if h.OpCount, err = d.Uint32(); err != nil {
return h, nil, err
}
if h.OpCount > maxOps {
return h, nil, fmt.Errorf("nfs4: %d operations exceeds %d", h.OpCount, maxOps)
}
return h, d, nil
}
// AppendCompoundRes encodes the COMPOUND4res: the top level status, the
// echoed tag and the result array. On success the results hold one entry
// per executed operation; on failure they hold the results up to and
// including the operation that failed, and the top level status carries its
// status.
func AppendCompoundRes(b []byte, status uint32, tag string, results [][]byte) []byte {
b = xdr.AppendUint32(b, status)
b = xdr.AppendString(b, tag)
b = xdr.AppendUint32(b, uint32(len(results)))
for _, op := range results {
b = append(b, op...)
}
return b
}
// AppendOpHeader appends the head of one nfs_resop4: the operation number
// and its status. A successful result appends its payload afterwards; a
// failed one is complete as it stands.
func AppendOpHeader(b []byte, op, status uint32) []byte {
b = xdr.AppendUint32(b, op)
return xdr.AppendUint32(b, status)
}
// A ResOp is one decoded result operation: its number, its status, and the
// bytes that follow the status. Body is meaningful only while the status is
// NFS4ERR_OK, and its shape depends on the operation.
type ResOp struct {
Op uint32
Status uint32
Body []byte
}
// A CompoundRes is the decoded COMPOUND4res.
type CompoundRes struct {
Status uint32
Tag string
Ops []ResOp
}
// DecodeCompoundResBodies decodes a COMPOUND4res and, for every operation,
// the bytes between its status and the next operation number. A body is
// empty when the operation carried no payload or when it failed. The
// bodies are not length prefixed on the wire: the length of each follows
// from its operation number, so the decoder measures each body as it goes.
func DecodeCompoundResBodies(payload []byte) (CompoundRes, [][]byte, error) {
d := xdr.NewDecoder(payload)
var res CompoundRes
var err error
if res.Status, err = d.Uint32(); err != nil {
return res, nil, err
}
if res.Tag, err = d.String(); err != nil {
return res, nil, err
}
count, err := d.Uint32()
if err != nil {
return res, nil, err
}
if count > maxOps {
return res, nil, fmt.Errorf("nfs4: %d results exceeds %d", count, maxOps)
}
var ops []ResOp
var bodies [][]byte
for range count {
var op ResOp
if op.Op, err = d.Uint32(); err != nil {
return res, nil, err
}
if op.Status, err = d.Uint32(); err != nil {
return res, nil, err
}
start := len(payload) - d.Remaining()
n, err := resBodyLen(op.Op, op.Status, payload[start:])
if err != nil {
return res, nil, err
}
if n < 0 || start+n > len(payload) {
return res, nil, fmt.Errorf("nfs4: result body of %d bytes overruns the payload", n)
}
op.Body = payload[start : start+n]
if _, err := d.Raw(n); err != nil {
return res, nil, err
}
ops = append(ops, op)
bodies = append(bodies, op.Body)
}
if d.Remaining() > 0 {
return res, nil, fmt.Errorf("nfs4: %d bytes trail the declared operations", d.Remaining())
}
res.Ops = ops
return res, bodies, nil
}
// resBodyLen measures how many bytes one result body occupies. A failed
// operation carries no body. An operation this build cannot measure, with a
// successful status, is a response the client has no business receiving.
func resBodyLen(op, status uint32, body []byte) (int, error) {
// Every failed operation carries an empty body, except LOCKT: its
// DENIED answer names the conflicting lock.
if status != ErrOK && op != OpLockt {
return 0, nil
}
d := xdr.NewDecoder(body)
switch op {
case OpPutfh, OpPutRootfh, OpLookup, OpLookupp, OpSavefh, OpRestorefh:
return 0, nil
case OpGetfh:
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
case OpGetattr:
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
case OpAccess:
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
case OpCreate:
// changeinfo4 (atomic, before, after) then the bitmap of the
// attributes set, RFC 8881 section 18.4.2.
if _, err := d.Bool(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
case OpWrite:
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Raw(8); err != nil {
return 0, err
}
case OpReadlink:
// The link text, length prefixed.
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
return len(body) - d.Remaining(), nil
case OpCommit:
// The write verifier.
return 8, nil
case OpSecinfo, OpSecinfoNoName:
// An array of secinfo4 unions: the flavour, and the
// rpcsec_gss_info body only on the RPCSEC_GSS branch.
n, err := d.Uint32()
if err != nil {
return 0, err
}
if n > maxOps {
return 0, ErrUnknownOp
}
for range n {
flavor, ferr := d.Uint32()
if ferr != nil {
return 0, ferr
}
if flavor == SecFlavorGSS {
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
}
}
return len(body) - d.Remaining(), nil
case OpRemove, OpOpenDowngrade:
// change_info4: an atomic flag and two 64 bit counters.
return 20, nil
case OpDestroyClientID, OpReclaimComplete, OpPutPubfh,
OpReleaseLockOwner, OpDelegReturn, OpFreeStateid, OpBackchannelCtl,
OpVerify, OpNverify:
return 0, nil
case OpBindConnToSession:
// sessionid and the granted direction.
return 20, nil
case OpTestStateid:
// One status per stateid the arguments named.
n, err := d.Uint32()
if err != nil {
return 0, err
}
if n > maxOps {
return 0, ErrUnknownOp
}
return 4 + 4*int(n), nil
case OpRename:
// Two change_info4 values, one per directory.
return 40, nil
case OpLink:
// change_info4 of the target directory.
return 20, nil
case OpSequence:
// sessionid and the five words of SEQUENCE4resok, RFC 7863.
if _, err := d.Raw(16); err != nil {
return 0, err
}
for range 5 {
if _, err := d.Uint32(); err != nil {
return 0, err
}
}
return 36, nil
case OpDestroySession:
return 0, nil
case OpExchangeID:
// clientid, sequence, flags, state protect how, eir_server_owner
// (uint64 minor id, opaque major id), eir_server_scope and the
// impl_id array, RFC 8881 section 18.35.
if _, err := d.Uint64(); err != nil {
return 0, err
}
for range 3 {
if _, err := d.Uint32(); err != nil {
return 0, err
}
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
for range 2 { // so_major_id then server_scope
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
}
n, err := d.Uint32()
if err != nil {
return 0, err
}
for range n {
for range 2 { // domain and name
if _, err := d.String(); err != nil {
return 0, err
}
}
if _, err := d.Raw(12); err != nil { // nfstime4
return 0, err
}
}
return len(body) - d.Remaining(), nil
case OpCreateSession:
// sessionid, sequence, flags and two channel attrs.
if _, err := d.Raw(16); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
for range 2 {
if _, err := d.Uint32(); err != nil {
return 0, err
}
for range 5 {
if _, err := d.Uint32(); err != nil {
return 0, err
}
}
count, cerr := d.Uint32()
if cerr != nil {
return 0, cerr
}
if count != 0xffffffff {
for range count {
if _, err := d.Uint32(); err != nil {
return 0, err
}
}
}
}
return len(body) - d.Remaining(), nil
case OpOpen:
// stateid, change_info4, rflags, attrset and the delegation union.
if _, err := d.Raw(16); err != nil {
return 0, err
}
if _, err := d.Bool(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
delegType, derr := d.Uint32()
if derr != nil {
return 0, derr
}
switch delegType {
case OpenDelegRead:
// stateid, recall flag and the permissions ace.
if _, err := d.Raw(16); err != nil {
return 0, err
}
if _, err := d.Bool(); err != nil {
return 0, err
}
if err := walkACE(d); err != nil {
return 0, err
}
case OpenDelegWrite:
// stateid, recall flag, the space limit union and the ace.
if _, err := d.Raw(16); err != nil {
return 0, err
}
if _, err := d.Bool(); err != nil {
return 0, err
}
if err := walkSpaceLimit(d); err != nil {
return 0, err
}
if err := walkACE(d); err != nil {
return 0, err
}
}
return len(body) - d.Remaining(), nil
case OpClose:
return 16, nil
case OpLock, OpLocku:
// The stateid.
return 16, nil
case OpLockt:
return resBodyLenLockt(status, body)
case OpSetattr:
// attrsset: only a bitmap4.
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
case OpRead:
if _, err := d.Bool(); err != nil {
return 0, err
}
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
case OpReaddir:
if _, err := d.Raw(8); err != nil {
return 0, err
}
for {
more, err := d.Bool()
if err != nil {
return 0, err
}
if !more {
break
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.String(); err != nil {
return 0, err
}
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
}
if _, err := d.Bool(); err != nil {
return 0, err
}
case OpLayoutGet:
// The layout stateid, the return on close flag and the layout
// array: offset, length, IO mode, type and body per segment.
if _, err := d.Raw(16); err != nil {
return 0, err
}
if _, err := d.Bool(); err != nil {
return 0, err
}
n, err := d.Uint32()
if err != nil {
return 0, err
}
for range n {
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
}
case OpLayoutCommit:
// The newsize4 union: the changed flag and the size.
if _, err := d.Bool(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
case OpLayoutReturn:
// The layoutreturn_stateid4 union: the returned flag and the
// stateid.
if _, err := d.Bool(); err != nil {
return 0, err
}
if _, err := d.Raw(16); err != nil {
return 0, err
}
case OpSeek:
// The end of file flag and the offset.
if _, err := d.Bool(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
case OpAllocate, OpDeallocate, OpClone, OpLayoutError, OpLayoutStats, OpOffloadCancel:
return 0, nil
case OpIoAdvise:
// The accepted hints bitmap4.
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
case OpCopy:
// write_response4 then the two requirement flags.
n, err := d.Uint32()
if err != nil {
return 0, err
}
if n == 1 {
if _, err := d.Raw(16); err != nil {
return 0, err
}
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Raw(8); err != nil {
return 0, err
}
if _, err := d.Bool(); err != nil {
return 0, err
}
if _, err := d.Bool(); err != nil {
return 0, err
}
case OpCopyNotify:
// lease time, empty owner confirm, stateid, source list.
if _, err := d.Int64(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Raw(16); err != nil {
return 0, err
}
n, err := d.Uint32()
if err != nil {
return 0, err
}
for range n {
kind, kerr := d.Uint32()
if kerr != nil {
return 0, kerr
}
switch kind {
case 1, 2:
if _, err := d.String(); err != nil {
return 0, err
}
default:
if _, err := d.String(); err != nil {
return 0, err
}
if _, err := d.String(); err != nil {
return 0, err
}
}
}
case OpOffloadStatus:
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
case OpReadPlus:
// eof flag and the content array.
if _, err := d.Bool(); err != nil {
return 0, err
}
n, err := d.Uint32()
if err != nil {
return 0, err
}
for range n {
kind, kerr := d.Uint32()
if kerr != nil {
return 0, kerr
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if kind == uint32(ContentData) {
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
} else if _, err := d.Uint64(); err != nil {
return 0, err
}
}
case OpWriteSame:
// write_response4 without a callback arm.
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Raw(8); err != nil {
return 0, err
}
case OpGetDirDelegation:
// The non fatal union: the status, then the resok body or the
// will signal flag.
gdd, err := d.Uint32()
if err != nil {
return 0, err
}
if gdd == GddOK {
if _, err := d.Raw(24); err != nil {
return 0, err
}
for range 3 {
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
}
} else if _, err := d.Bool(); err != nil {
return 0, err
}
case OpOpenattr:
return 0, nil
case OpGetDeviceList:
// The cookie, its verifier, the device id array and the eof flag.
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Raw(8); err != nil {
return 0, err
}
n, err := d.Uint32()
if err != nil {
return 0, err
}
if _, err := d.Raw(int(n) * 16); err != nil {
return 0, err
}
if _, err := d.Bool(); err != nil {
return 0, err
}
case OpGetxattr:
// The single opaque value.
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
case OpSetxattr, OpRemovexattr:
return 0, nil
case OpListxattr:
// The cookie of the last name, the names and the end of list
// flag.
if _, err := d.Uint64(); err != nil {
return 0, err
}
n, err := d.Uint32()
if err != nil {
return 0, err
}
for range n {
if _, err := d.String(); err != nil {
return 0, err
}
}
if _, err := d.Bool(); err != nil {
return 0, err
}
case OpGetDeviceInfo:
// The address type, the address body and the notification bitmap.
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
if _, err := ReadBitmap(d); err != nil {
return 0, err
}
default:
return 0, ErrUnknownOp
}
return len(body) - d.Remaining(), nil
}
// walkACE walks one nfsace4 of a result body.
func walkACE(d *xdr.Decoder) error {
for range 3 {
if _, err := d.Uint32(); err != nil {
return err
}
}
_, err := d.VarOpaque()
return err
}
// walkSpaceLimit walks one nfs_space_limit4 union of a result body.
func walkSpaceLimit(d *xdr.Decoder) error {
limitBy, err := d.Uint32()
if err != nil {
return err
}
switch limitBy {
case LimitBySize:
_, err = d.Uint64()
return err
case 2: // NFS_LIMIT_BLOCKS
if _, err = d.Uint32(); err != nil {
return err
}
_, err = d.Uint32()
return err
default:
return ErrBadDeleg
}
}
// ErrUnknownOp marks a successful result whose shape this build cannot
// measure: the client and the server disagree on what was sent.
var ErrUnknownOp = errors.New("nfs4: unknown operation result")
// resBodyLenCB measures one CB result body. CB operation numbers share
// values with the fore channel, so the two families are never mixed in
// one measurement.
func resBodyLenCB(op, status uint32, body []byte) (int, error) {
if status != ErrOK {
return 0, nil
}
d := xdr.NewDecoder(body)
switch op {
case OpCBSequence:
// The session id and the four words of CB_SEQUENCE4resok,
// RFC 7863.
if _, err := d.Raw(16); err != nil {
return 0, err
}
for range 4 {
if _, err := d.Uint32(); err != nil {
return 0, err
}
}
return 32, nil
case OpCBRecall, OpCBNotify, OpCBNotifyLock:
return 0, nil
default:
return 0, nil
}
}
// DecodeCompoundResBodiesCB decodes a CB_COMPOUND reply: the same walk as
// the fore channel variant, with the callback operation shapes.
func DecodeCompoundResBodiesCB(payload []byte) (CompoundRes, [][]byte, error) {
d := xdr.NewDecoder(payload)
var res CompoundRes
var err error
if res.Status, err = d.Uint32(); err != nil {
return res, nil, err
}
if res.Tag, err = d.String(); err != nil {
return res, nil, err
}
count, err := d.Uint32()
if err != nil {
return res, nil, err
}
if count > maxOps {
return res, nil, fmt.Errorf("nfs4: %d results exceeds %d", count, maxOps)
}
var ops []ResOp
var bodies [][]byte
for range count {
var op ResOp
if op.Op, err = d.Uint32(); err != nil {
return res, nil, err
}
if op.Status, err = d.Uint32(); err != nil {
return res, nil, err
}
start := len(payload) - d.Remaining()
n, err := resBodyLenCB(op.Op, op.Status, payload[start:])
if err != nil {
return res, nil, err
}
if n < 0 || start+n > len(payload) {
return res, nil, fmt.Errorf("nfs4: result body of %d bytes overruns the payload", n)
}
op.Body = payload[start : start+n]
if _, err := d.Raw(n); err != nil {
return res, nil, err
}
ops = append(ops, op)
bodies = append(bodies, op.Body)
}
if d.Remaining() > 0 {
return res, nil, fmt.Errorf("nfs4: %d bytes trail the declared operations", d.Remaining())
}
res.Ops = ops
return res, bodies, nil
}
+256
View File
@@ -0,0 +1,256 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Package nfs4 carries the NFSv4.2 wire vocabulary: the program and
// operation numbers, the error codes, the attribute table, and the
// encoding of the COMPOUND procedure and its operations.
//
// The constants follow RFC 8881 and RFC 7862. Only operations implemented
// here are named; the dispatcher answers everything else with
// NFS4ERR_NOT_SUPP.
package nfs4
// The NFS program of RFC 8881. Version 4 is the only RPC program version
// the NFSv4 family uses; the minor version travels inside COMPOUND.
const (
Program = 100003
Version = 4
MinorVersion = 2
ProcNull = 0
ProcCompound = 1
)
// Operation numbers, RFC 8881 section 15. Operations 1 and 2 are unassigned.
const (
OpAccess = 3
OpClose = 4
OpCommit = 5
OpCreate = 6
OpDelegPurge = 7
OpDelegReturn = 8
OpGetattr = 9
OpGetfh = 10
OpLink = 11
OpLock = 12
OpLockt = 13
OpLocku = 14
OpLookup = 15
OpLookupp = 16
OpNverify = 17
OpOpen = 18
OpOpenattr = 19
OpOpenConfirm = 20
OpOpenDowngrade = 21
OpPutfh = 22
OpPutPubfh = 23
OpPutRootfh = 24
OpRead = 25
OpReaddir = 26
OpReadlink = 27
OpRemove = 28
OpRename = 29
OpRenew = 30
OpRestorefh = 31
OpSavefh = 32
OpSecinfo = 33
OpSetattr = 34
OpSetClientID = 35
OpSetClientIDConfirm = 36
OpVerify = 37
OpWrite = 38
OpReleaseLockOwner = 39
OpBackchannelCtl = 40
OpBindConnToSession = 41
OpExchangeID = 42
OpCreateSession = 43
OpDestroySession = 44
OpFreeStateid = 45
OpGetDirDelegation = 46
OpGetDeviceInfo = 47
OpGetDeviceList = 48
OpLayoutCommit = 49
OpLayoutGet = 50
OpLayoutReturn = 51
OpSecinfoNoName = 52
OpSequence = 53
OpSetSsv = 54
OpTestStateid = 55
OpWantDelegation = 56
OpDestroyClientID = 57
OpReclaimComplete = 58
OpAllocate = 59
OpCopy = 60
OpCopyNotify = 61
OpDeallocate = 62
OpIoAdvise = 63
OpLayoutError = 64
OpLayoutStats = 65
OpOffloadCancel = 66
OpOffloadStatus = 67
OpReadPlus = 68
OpSeek = 69
OpWriteSame = 70
OpClone = 71
OpGetxattr = 72
OpSetxattr = 73
OpListxattr = 74
OpRemovexattr = 75
OpIllegal = 10044
)
// NFS4ERR statuses. Every number here was verified against the error
// table of RFC 8881 section 15.1; several plausible looking values are
// wrong by a few dozen, which real clients answer with a mismatch.
const (
ErrOK = 0
ErrPerm = 1
ErrNoEnt = 2
ErrIO = 5
ErrNXIO = 6
ErrAccess = 13
ErrExist = 17
ErrXDev = 18
ErrNotDir = 20
ErrIsDir = 21
ErrInval = 22
ErrFBig = 27
ErrNoSpc = 28
ErrROFS = 30
ErrNameTooLong = 63
ErrNotEmpty = 66
ErrDQuot = 69
ErrStale = 70
ErrBadHandle = 10001
ErrNotSupp = 10004
ErrTooSmall = 10005
ErrServerFault = 10006
ErrBadType = 10007
ErrDelay = 10008
ErrSame = 10009
ErrDenied = 10010
ErrExpired = 10011
ErrLocked = 10012
ErrGrace = 10013
ErrNoGrace = 10033
ErrLocksHeld = 10037
ErrFHExpired = 10014
ErrShareDenied = 10015
ErrWrongSec = 10016
ErrClidInUse = 10017
ErrResource = 10018
ErrMoved = 10019
ErrNoFileHandle = 10020
ErrMinorVersMismatch = 10021
ErrStaleClientID = 10022
ErrOldStateid = 10024
ErrBadStateid = 10025
ErrBadSeqid = 10026
ErrNotSame = 10027
ErrAttrNotSupp = 10032
ErrDeadLock = 10045
ErrFileOpen = 10046
ErrOpIllegal = 10044
ErrBadName = 10041
ErrBadSession = 10052
ErrBadSlot = 10053
ErrSequencePos = 10064
ErrCompleteAlready = 10054
ErrConnNotBound = 10055
ErrNotOnlyOp = 10081
ErrOpNotInSession = 10071
ErrSeqMisordered = 10063
ErrRetryUncachedRep = 10068
ErrClientIDBusy = 10074
ErrDeadSession = 10078
ErrBadIOMode = 10049
ErrBadLayout = 10050
ErrNoMatchingLayout = 10060
ErrUnknownLayoutType = 10062
ErrPnfsNoLayout = 10080
ErrNoXattr = 10095
ErrXattr2Big = 10096
ErrWrongType = 10083
ErrBadOwner = 10093
)
// Layout return kinds of the layoutreturn4 returntype switch, RFC 8881
// section 18.44.
const (
ReturnFile = 1
ReturnFsid = 2
ReturnAll = 3
)
// File types of the fattr4 type attribute.
const (
NF4Reg = 1
NF4Dir = 2
NF4Blk = 3
NF4Chr = 4
NF4Lnk = 5
NF4Sock = 6
NF4Fifo = 7
)
// Access mask bits, RFC 8881 section 15.2.2.
const (
AccessRead = 1 << 0
AccessLookup = 1 << 1
AccessModify = 1 << 2
AccessExtend = 1 << 3
AccessDelete = 1 << 4
AccessExec = 1 << 5
)
// Write stability levels, RFC 8881 section 15.8. The server always answers
// FILE_SYNC, so the client needs no replay bookkeeping.
const (
StableUnstable = 0
StableDataSync = 1
StableFileSync = 2
)
// CREATE modes of the createhow4 union.
const (
CreateUnchecked = 0
CreateGuarded = 1
CreateExclusive = 2
CreateExclusive1 = 3 // EXCLUSIVE4_1 of RFC 8881
)
// File handle expiry classes. This server serves handles that survive as
// long as the server process does.
const (
FH4Persistent = 0
FH4VolatileAny = 1
FH4VolatileMigrate = 2
FH4VolatileNoMigrate = 3
)
// A Stateid is the 16 byte state identifier of RFC 8881 section 8.2: a
// 32 bit sequence number followed by a 12 byte other field. The all zero
// stateid is the anonymous stateid.
type Stateid [16]byte
// AllZero is the anonymous stateid: no lock state applies to the request.
var AllZero Stateid
// Append appends the stateid to b in wire order.
func (s Stateid) Append(b []byte) []byte {
return append(b, s[:]...)
}
// Security flavours a SECINFO answer names, the AUTH values of RFC 5531.
const (
SecFlavorNone = 0
SecFlavorSys = 1
SecFlavorGSS = 6
)
// The secinfo_style4 styles of RFC 8881 section 18.44: the current file
// handle, or its parent directory.
const (
StyleCurrentFH = 0 // SECINFO_STYLE4_CURRENT_FH
StyleParent = 1 // SECINFO_STYLE4_PARENT
)
+100
View File
@@ -0,0 +1,100 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// The numbered session id: the prefix, the number and the client id
// halves, RFC 8881 section 18.36.
func TestMakeNumberedSessionID(t *testing.T) {
id := MakeNumberedSessionID([4]byte{9, 9, 9, 9}, 0x11223344, 0x5566778899aabbcc)
if id[0] != 9 || id[3] != 9 {
t.Fatalf("prefix % x", id[:4])
}
if id[4] != 0x11 || id[7] != 0x44 {
t.Fatalf("number % x", id[4:8])
}
if id.ClientIDOf() != 0x5566778899aabbcc {
t.Fatalf("clientid %x", id.ClientIDOf())
}
// Every number mints a distinct id.
if MakeNumberedSessionID([4]byte{}, 1, 5) == MakeNumberedSessionID([4]byte{}, 2, 5) {
t.Fatal("two numbers minted the same session id")
}
}
// NegotiateChannel clamps each value to the smaller of the request and
// the limit and leaves a zero request at zero.
func TestNegotiateChannel(t *testing.T) {
got := NegotiateChannel(
ChannelAttrs{MaxRequest: 1 << 30, MaxResponse: 1 << 10, MaxRespResourced: 1 << 10,
MaxOperations: 64, MaxRequests: 0},
ChannelAttrs{MaxRequest: 1 << 22, MaxResponse: 1 << 22, MaxRespResourced: 1 << 22,
MaxOperations: 16, MaxRequests: 8},
)
want := ChannelAttrs{MaxRequest: 1 << 22, MaxResponse: 1 << 10, MaxRespResourced: 1 << 10,
MaxOperations: 16, MaxRequests: 0}
if got.MaxRequest != want.MaxRequest || got.MaxResponse != want.MaxResponse ||
got.MaxRespResourced != want.MaxRespResourced || got.MaxOperations != want.MaxOperations ||
got.MaxRequests != want.MaxRequests {
t.Fatalf("negotiated %+v, want %+v", got, want)
}
}
// The EXCLUSIVE4_1 open args round trip: guarded create, verifier
// consumed, mode carried.
func TestOpenArgsExclusive41RoundTrip(t *testing.T) {
arg := AppendOpenArgsExclusive41(nil, 0x77, []byte("ex"),
[8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 0o640, "ex41.txt")
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpOpen {
t.Fatalf("op %d %v", op, err)
}
a, err := DecodeOpenArgs(d)
if err != nil {
t.Fatal(err)
}
if !a.Create || !a.Guarded || a.Truncate || a.Perm != 0o640 || a.Name != "ex41.txt" ||
a.ClientID != 0x77 || string(a.Owner) != "ex" {
t.Fatalf("args %+v", a)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
// The guarded form sets the flag the same way.
arg = AppendOpenArgs(nil, 1, []byte("o"), ShareAccessRead, 0, true, 0o600, "g.txt")
if _, err := DecodeOpenArgs(xdr.NewDecoder(arg[4:])); err != nil {
t.Fatal(err)
}
}
// The OPEN result measurement walks the write delegation with its space
// limit union and the permissions ace.
func TestResBodyLenOpenDelegations(t *testing.T) {
var st Stateid
st[0] = 1
write := AppendOpenResDeleg(nil, st, OpenDelegWrite, st)
if n, err := resBodyLen(OpOpen, ErrOK, write); err != nil || n != len(write) {
t.Fatalf("write deleg measured %d of %d: %v", n, len(write), err)
}
read := AppendOpenResDeleg(nil, st, OpenDelegRead, st)
if n, err := resBodyLen(OpOpen, ErrOK, read); err != nil || n != len(read) {
t.Fatalf("read deleg measured %d of %d: %v", n, len(read), err)
}
none := AppendOpenRes(nil, st)
if n, err := resBodyLen(OpOpen, ErrOK, none); err != nil || n != len(none) {
t.Fatalf("no deleg measured %d of %d: %v", n, len(none), err)
}
// A truncated body of any of the three fails instead of measuring
// past the payload.
for n := 0; n < len(write); n += 4 {
if _, err := resBodyLen(OpOpen, ErrOK, write[:n]); err == nil {
t.Fatalf("a %d byte prefix measured cleanly", n)
}
}
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Directory delegations and the notification family: the wire codecs of
// GET_DIR_DELEGATION (RFC 8881 section 18.39), CB_NOTIFY and
// CB_NOTIFY_LOCK (RFC 8881 section 20), with the shapes taken from the
// XDR of RFC 7863.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Notification types of the notify_type4 enum.
const (
NotifyChangeChildAttrs = 0
NotifyChangeDirAttrs = 1
NotifyRemoveEntry = 2
NotifyAddEntry = 3
NotifyRenameEntry = 4
NotifyChangeCookieVerf = 5
)
// Statuses of the gddrnf4_status union.
const (
GddOK = 0
GddUnavail = 1
)
// A Notify4 is one notify4: the kind of the change and the encoded
// change body.
type Notify4 struct {
Mask Bitmap
Vals []byte
}
// A CBNotify is the decoded CB_NOTIFY4args.
type CBNotify struct {
Stateid Stateid
FH []byte
Changes []Notify4
}
// A CBNotifyLock is the decoded CB_NOTIFY_LOCK4args.
type CBNotifyLock struct {
FH []byte
Clientid uint64
Owner []byte
}
// AppendGetDirDelegationArgs encodes the GET_DIR_DELEGATION4args: the
// signal flag, the notification types, the attr delay hints and the
// requested attribute bitmaps.
func AppendGetDirDelegationArgs(b []byte, signalAvail bool, notifyTypes Bitmap, childDelay, dirDelay NfsTime, childAttrs, dirAttrs Bitmap) []byte {
b = xdr.AppendUint32(b, OpGetDirDelegation)
b = xdr.AppendBool(b, signalAvail)
b = notifyTypes.AppendTo(b)
b = appendNfsTime(b, childDelay)
b = appendNfsTime(b, dirDelay)
b = childAttrs.AppendTo(b)
return dirAttrs.AppendTo(b)
}
// AppendGetDirDelegationResOK encodes the delegation granted: the
// verifier, the stateid, the supported notifications and the attribute
// bitmaps the server promises.
func AppendGetDirDelegationResOK(b []byte, cookieverf [8]byte, st Stateid, notification, childAttrs, dirAttrs Bitmap) []byte {
b = xdr.AppendUint32(b, GddOK)
b = append(b, cookieverf[:]...)
b = st.Append(b)
b = notification.AppendTo(b)
b = childAttrs.AppendTo(b)
return dirAttrs.AppendTo(b)
}
// AppendGetDirDelegationResUnavail encodes the no delegation answer: the
// server promises to signal when one becomes available.
func AppendGetDirDelegationResUnavail(b []byte, willSignal bool) []byte {
b = xdr.AppendUint32(b, GddUnavail)
return xdr.AppendBool(b, willSignal)
}
// appendNotifyEntry encodes one notify_entry4: the name and the full
// attribute list, which this build carries empty.
func appendNotifyEntry(b []byte, name string) []byte {
b = xdr.AppendString(b, name)
return AppendFattr(b, Bitmap{}, Attrs{})
}
// AppendNotifyAdd encodes the notify_add4 body: the optional removed
// source of a rename, the new entry, its cookie, the optional previous
// entry and the end of page flag.
func AppendNotifyAdd(b []byte, name string, cookie uint64, lastEntry bool) []byte {
b = xdr.AppendUint32(b, 0) // nad_old_entry<1>: none
b = appendNotifyEntry(b, name)
b = xdr.AppendUint32(b, 0) // nad_new_entry_cookie<1>: none
b = xdr.AppendUint32(b, 0) // nad_prev_entry<1>: none
return xdr.AppendBool(b, lastEntry)
}
// AppendNotifyRemove encodes the notify_remove4 body: the removed entry
// and the cookie the client resumes past it from.
func AppendNotifyRemove(b []byte, name string, cookie uint64) []byte {
b = appendNotifyEntry(b, name)
return xdr.AppendUint64(b, cookie)
}
// AppendNotify4 encodes one notify4: the mask and the encoded body.
func AppendNotify4(b []byte, mask Bitmap, vals []byte) []byte {
b = mask.AppendTo(b)
return xdr.AppendVarOpaque(b, vals)
}
// AppendCBNotifyArgs encodes the CB_NOTIFY4args: the delegation stateid,
// the directory and the changes.
func AppendCBNotifyArgs(b []byte, st Stateid, fh []byte, changes []Notify4) []byte {
b = xdr.AppendUint32(b, OpCBNotify)
b = st.Append(b)
b = xdr.AppendVarOpaque(b, fh)
b = xdr.AppendUint32(b, uint32(len(changes)))
for _, c := range changes {
b = AppendNotify4(b, c.Mask, c.Vals)
}
return b
}
// DecodeCBNotifyArgs decodes the CB_NOTIFY4args the client receives.
func DecodeCBNotifyArgs(d *xdr.Decoder) (CBNotify, error) {
var out CBNotify
raw, err := d.Raw(16)
if err != nil {
return out, err
}
copy(out.Stateid[:], raw)
if out.FH, err = d.VarOpaque(); err != nil {
return out, err
}
n, err := d.Uint32()
if err != nil {
return out, err
}
for range n {
var c Notify4
if c.Mask, err = ReadBitmap(d); err != nil {
return out, err
}
if c.Vals, err = d.VarOpaque(); err != nil {
return out, err
}
out.Changes = append(out.Changes, c)
}
return out, nil
}
// AppendCBNotifyLockArgs encodes the CB_NOTIFY_LOCK4args: the file and
// the lock owner whose denied lock may be available again.
func AppendCBNotifyLockArgs(b []byte, fh []byte, clientid uint64, owner []byte) []byte {
b = xdr.AppendUint32(b, OpCBNotifyLock)
b = xdr.AppendVarOpaque(b, fh)
b = xdr.AppendUint64(b, clientid)
return xdr.AppendVarOpaque(b, owner)
}
// DecodeCBNotifyLockArgs decodes the CB_NOTIFY_LOCK4args the client
// receives.
func DecodeCBNotifyLockArgs(d *xdr.Decoder) (CBNotifyLock, error) {
var out CBNotifyLock
fh, err := d.VarOpaque()
if err != nil {
return out, err
}
out.FH = fh
if out.Clientid, err = d.Uint64(); err != nil {
return out, err
}
out.Owner, err = d.VarOpaque()
return out, err
}
+139
View File
@@ -0,0 +1,139 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestDirDelegationShapes(t *testing.T) {
t.Parallel()
st := Stateid{1, 'D', 'D', 'E'}
args := AppendGetDirDelegationArgs(nil, true, OfBits(NotifyAddEntry),
NfsTime{Seconds: 5, Nseconds: 6}, NfsTime{Seconds: 7, Nseconds: 8},
Bitmap{}, Bitmap{})
d := xdr.NewDecoder(args[4:])
if sig, err := d.Bool(); err != nil || !sig {
t.Fatalf("signal %v: %v", sig, err)
}
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
for _, want := range []NfsTime{{Seconds: 5, Nseconds: 6}, {Seconds: 7, Nseconds: 8}} {
s, err := d.Int64()
if err != nil || s != want.Seconds {
t.Fatalf("delay seconds %d: %v", s, err)
}
ns, err := d.Uint32()
if err != nil || ns != want.Nseconds {
t.Fatalf("delay nseconds %d: %v", ns, err)
}
}
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the dir delegation args", d.Remaining())
}
ok := AppendGetDirDelegationResOK(nil, [8]byte{9}, st,
OfBits(NotifyAddEntry), Bitmap{}, Bitmap{})
n, err := resBodyLen(OpGetDirDelegation, ErrOK, ok)
if err != nil || n != len(ok) {
t.Fatalf("resok measured %d of %d: %v", n, len(ok), err)
}
un := AppendGetDirDelegationResUnavail(nil, true)
if n, err := resBodyLen(OpGetDirDelegation, ErrOK, un); err != nil || n != len(un) {
t.Fatalf("unavail measured %d of %d: %v", n, len(un), err)
}
d = xdr.NewDecoder(un)
if s, err := d.Uint32(); err != nil || s != GddUnavail {
t.Fatalf("unavail status %d: %v", s, err)
}
}
func TestNotifyShapes(t *testing.T) {
t.Parallel()
add := AppendNotifyAdd(nil, "entry", 0, true)
d := xdr.NewDecoder(add)
if n, err := d.Uint32(); err != nil || n != 0 {
t.Fatalf("old entries %d: %v", n, err)
}
if name, err := d.String(); err != nil || name != "entry" {
t.Fatalf("name %q: %v", name, err)
}
if _, err := ReadBitmap(d); err != nil { // empty fattr bitmap
t.Fatal(err)
}
if _, err := d.VarOpaque(); err != nil { // empty fattr blob
t.Fatal(err)
}
for range 2 {
if _, err := d.Uint32(); err != nil {
t.Fatal(err)
}
}
if last, err := d.Bool(); err != nil || !last {
t.Fatalf("last %v: %v", last, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the add body", d.Remaining())
}
rem := AppendNotifyRemove(nil, "gone", 7)
d = xdr.NewDecoder(rem)
if name, err := d.String(); err != nil || name != "gone" {
t.Fatalf("name %q: %v", name, err)
}
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if _, err := d.VarOpaque(); err != nil {
t.Fatal(err)
}
if c, err := d.Uint64(); err != nil || c != 7 {
t.Fatalf("cookie %d: %v", c, err)
}
// CB_NOTIFY args round trip through the decoder the client uses.
st := Stateid{2, 'N'}
fh := []byte("dir-fh")
changes := []Notify4{{Mask: OfBits(NotifyAddEntry), Vals: add}}
cb := AppendCBNotifyArgs(nil, st, fh, changes)
if n, err := resBodyLenCB(OpCBSequence, ErrOK, make([]byte, 32)); err != nil {
t.Fatal(err)
} else if n != 32 {
t.Fatalf("cb sequence body %d", n)
}
got, err := DecodeCBNotifyArgs(xdr.NewDecoder(cb[4:]))
if err != nil {
t.Fatal(err)
}
if got.Stateid != st || !bytes.Equal(got.FH, fh) || len(got.Changes) != 1 {
t.Fatalf("decoded %+v", got)
}
if !got.Changes[0].Mask.Has(NotifyAddEntry) {
t.Fatalf("mask %v", got.Changes[0].Mask)
}
// CB_NOTIFY_LOCK args round trip.
nl := AppendCBNotifyLockArgs(nil, fh, 0x77, []byte("owner"))
lock, err := DecodeCBNotifyLockArgs(xdr.NewDecoder(nl[4:]))
if err != nil {
t.Fatal(err)
}
if lock.Clientid != 0x77 || string(lock.Owner) != "owner" || !bytes.Equal(lock.FH, fh) {
t.Fatalf("decoded %+v", lock)
}
if n, err := resBodyLenCB(OpCBNotifyLock, ErrOK, nil); err != nil || n != 0 {
t.Fatalf("notify lock res body %d: %v", n, err)
}
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// FuzzDecodeCompound feeds arbitrary compound payloads through the
// argument and result walkers: no input may panic, and every malformed
// body must arrive as an error, never as a wrong split.
func FuzzDecodeCompound(f *testing.F) {
args := AppendCompoundArgs(nil, "seed", MinorVersion, [][]byte{
AppendSequenceArgs(nil, SessionID{1}, 1, 0, 0, true),
AppendPutRootfh(nil),
AppendLookup(nil, "a"),
})
f.Add(args)
res := AppendCompoundRes(nil, ErrOK, "seed", [][]byte{
append(AppendOpHeader(nil, OpSequence, ErrOK), AppendSequenceRes(nil, SessionID{1}, 1, 0, 0, 0)...),
AppendOpHeader(nil, OpPutRootfh, ErrOK),
})
f.Add(res)
f.Add([]byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 75, 0, 0, 0, 0})
f.Add([]byte{0, 0, 0, 9, 0, 0, 0, 1, 0, 0, 0, 1})
f.Fuzz(func(t *testing.T, data []byte) {
// The property under test is that none of this panics; a
// malformed body is an ordinary error and a well formed one
// decodes with bodies that stay inside the payload.
_, d, err := DecodeCompoundArgs(data)
if err == nil {
for {
op, oerr := d.Uint32()
if oerr != nil || walkArgs(op, d) {
break
}
}
}
_, _, _ = DecodeCompoundResBodies(data)
_, _, _ = DecodeCompoundResBodiesCB(data)
})
}
// walkArgs walks one operation's arguments the way the dispatcher does,
// so the fuzzed stream exercises the real decoders. The boolean reports
// that the stream ended or was refused.
func walkArgs(op uint32, d *xdr.Decoder) bool {
switch op {
case OpExchangeID:
_, err := DecodeExchangeIDArgs(d)
return err != nil
case OpCreateSession:
_, err := DecodeCreateSessionArgs(d)
return err != nil
case OpSequence:
_, err := DecodeSequenceArgs(d)
return err != nil
case OpSecinfoNoName:
_, err := DecodeSecinfoNoNameArgs(d)
return err != nil
}
_, err := d.Uint32()
return err != nil
}
// TestRegistryValues pins the registry numbers this package speaks
// against the standards: an accidental renumbering of any of these
// breaks interoperation with every conformant peer, so the values
// themselves are the contract.
func TestRegistryValues(t *testing.T) {
pins := []struct {
name string
got uint32
want uint32
rfc string
}{
{"session id size", uint32(len(SessionID{})), 16, "RFC 7863"},
{"FATTR4_TIME_ACCESS_SET", AttrTimeAccessSet, 48, "RFC 7863"},
{"FATTR4_TIME_MODIFY_SET", AttrTimeModifySet, 54, "RFC 7863"},
{"LAYOUT4_FLEX_FILES", LayoutTypeFlexfiles, 4, "RFC 8435"},
{"LAYOUT4_NFSV4_1_FILES", LayoutTypeFiles, 1, "RFC 7863"},
{"LAYOUT4_OSD2_OBJECTS", LayoutTypeObjects, 2, "RFC 7863"},
{"LAYOUT4_BLOCK_VOLUME", LayoutTypeBlock, 3, "RFC 7863"},
{"layoutiomode4 RW", IoModeRW, 2, "RFC 7863"},
{"layoutiomode4 ANY", IoModeAny, 3, "RFC 7863"},
{"SP4_MACH_CRED", StateProtectMachCred, 1, "RFC 8881"},
{"SP4_SSV", StateProtectSSV, 2, "RFC 8881"},
{"SECINFO_STYLE4_CURRENT_FH", StyleCurrentFH, 0, "RFC 8881"},
{"SECINFO_STYLE4_PARENT", StyleParent, 1, "RFC 8881"},
{"NFS4ERR_NXIO", ErrNXIO, 6, "RFC 8881"},
{"NFS4ERR_BADOWNER", ErrBadOwner, 10093, "RFC 8881"},
{"NFS4ERR_NOXATTR", ErrNoXattr, 10095, "RFC 8276"},
{"NFS4ERR_XATTR2BIG", ErrXattr2Big, 10096, "RFC 8276"},
}
for _, p := range pins {
if p.got != p.want {
t.Errorf("%s: got %d, want %d per %s", p.name, p.got, p.want, p.rfc)
}
}
}
+191
View File
@@ -0,0 +1,191 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The layout type families beyond flexfiles, each with its wire body
// and the device address this build answers for it: files (RFC 5661
// section 13.3), block/volume (RFC 5663), objects (RFC 5664) and SCSI
// (RFC 8154). The metadata server of this build emulates every one of
// them over its own storage, so all bodies name the one device the
// server itself is.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// AppendFileLayoutBody encodes the nfsv4_1_file_layout4: the device,
// the stripe unit descriptor, the first stripe index, the pattern
// offset and the file handle list.
func AppendFileLayoutBody(b []byte, device [16]byte, util, firstStripe uint32, patternOffset uint64, fhs [][]byte) []byte {
b = append(b, device[:]...)
b = xdr.AppendUint32(b, util)
b = xdr.AppendUint32(b, firstStripe)
b = xdr.AppendUint64(b, patternOffset)
b = xdr.AppendUint32(b, uint32(len(fhs)))
for _, fh := range fhs {
b = xdr.AppendVarOpaque(b, fh)
}
return b
}
// AppendFileDeviceAddr encodes the nfsv4_1_file_layout_ds_addr4: the
// stripe indices over the multipath list of data server addresses.
func AppendFileDeviceAddr(b []byte, stripeIndices []uint32, addrs []NetAddr) []byte {
b = xdr.AppendUint32(b, uint32(len(stripeIndices)))
for _, i := range stripeIndices {
b = xdr.AppendUint32(b, i)
}
b = xdr.AppendUint32(b, uint32(len(addrs)))
for _, a := range addrs {
b = xdr.AppendString(b, a.Netid)
b = xdr.AppendString(b, a.Uaddr)
}
return b
}
// A BlockVolume is one pnfs_block_volume4 of the emulated device: a
// simple volume with the given base offset and block count.
type BlockVolume struct {
// Type is PNFS_BLOCK_VOLUME_SIMPLE (1).
BaseOffset uint64
BlockCount uint64
DeviceID [16]byte
}
// AppendBlockDeviceAddr encodes the pnfs_block_deviceaddr4 with one
// simple volume, RFC 5663 section 5.1.
func AppendBlockDeviceAddr(b []byte, v BlockVolume) []byte {
b = xdr.AppendUint32(b, 1) // one volume
b = xdr.AppendUint32(b, 1) // PNFS_BLOCK_VOLUME_SIMPLE
b = append(b, v.DeviceID[:]...)
b = xdr.AppendUint64(b, v.BaseOffset)
return xdr.AppendUint64(b, v.BlockCount)
}
// AppendScsiLayoutBody encodes the pnfs_scsi_layout4: one extent over
// the emulated volume, RFC 8154 section 5.2.
func AppendScsiLayoutBody(b []byte, device [16]byte, fileOffset, length, storageOffset uint64) []byte {
b = xdr.AppendUint32(b, 1) // one extent
b = append(b, device[:]...)
b = xdr.AppendUint64(b, fileOffset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendUint64(b, storageOffset)
return xdr.AppendUint32(b, 0) // reserved
}
// An ObjectLayout is the emulated pnfs_osd_layout4: the map header and
// one component credential over the emulated OSD.
type ObjectLayout struct {
NumComponents uint32
StripeUnit uint64
GroupWidth uint32
GroupDepth uint32
RefTagSize uint32
}
// AppendObjectLayoutBody encodes a minimal pnfs_osd_layout4 for the
// emulated OSD, RFC 5664 section 3.3: the data map and one component.
func AppendObjectLayoutBody(b []byte, device [16]byte, l ObjectLayout) []byte {
// pnfs_osd_data_map4
b = xdr.AppendUint32(b, 0) // odi_odm_raid_algorithm: PNFS_OSD_RAID_0
b = xdr.AppendUint64(b, l.StripeUnit)
b = xdr.AppendUint32(b, l.NumComponents)
b = xdr.AppendUint32(b, l.GroupWidth)
b = xdr.AppendUint32(b, l.GroupDepth)
b = xdr.AppendUint32(b, l.RefTagSize)
// olo_comps_index and the single component
b = xdr.AppendUint32(b, 0)
// pnfs_osd_object_cred4: version, credential type, opaque cred
b = xdr.AppendUint32(b, 0)
b = xdr.AppendUint32(b, 0)
b = xdr.AppendVarOpaque(b, nil)
// object id: partition id + object id
b = xdr.AppendUint64(b, 0)
b = xdr.AppendUint64(b, 1)
_ = device
return b
}
// AppendGetDeviceListRes encodes the GETDEVICELIST4resok: the cookie,
// its verifier, the device ids and the end of list flag, RFC 5661
// section 18.41.
func AppendGetDeviceListRes(b []byte, cookie uint64, cookieverf [8]byte, devices [][16]byte, eof bool) []byte {
b = xdr.AppendUint64(b, cookie)
b = append(b, cookieverf[:]...)
b = xdr.AppendUint32(b, uint32(len(devices)))
for _, d := range devices {
b = append(b, d[:]...)
}
return xdr.AppendBool(b, eof)
}
// AppendGetDeviceListArgs encodes the GETDEVICELIST argop: the layout
// type, the device count budget and the resumption cookie, RFC 5661
// section 18.41.
func AppendGetDeviceListArgs(b []byte, layoutType, maxDevices uint32, cookie uint64, cookieverf [8]byte) []byte {
b = xdr.AppendUint32(b, OpGetDeviceList)
b = xdr.AppendUint32(b, layoutType)
b = xdr.AppendUint32(b, maxDevices)
b = xdr.AppendUint64(b, cookie)
return append(b, cookieverf[:]...)
}
// LayoutTypeFlexFilesV2 is the layout type of the flexible file layout
// version 2: the layout type number 0x6 of the pNFS Layout Types
// registry, defined by the expired Internet-Draft
// draft-haynes-nfsv4-flex-filesv2-00. The draft never became an RFC;
// the number is stable and Linux clients carry the same one.
const LayoutTypeFlexFilesV2 = 6
// A FlexDataServerV2 is one ffv2_data_server4 of the draft: like the
// version one entry but with an array of stateids, the file handles
// per supported version, and the RPC credential the data server needs
// for loose coupling.
type FlexDataServerV2 struct {
DeviceID [16]byte
Efficiency uint32
Stateids []Stateid
FHs [][]byte
User string
Group string
AuthFlavor uint32
AuthBody []byte
}
// A FlexMirrorV2 is one ffv2_mirror4: the data servers of one mirror.
type FlexMirrorV2 struct {
DataServers []FlexDataServerV2
}
// AppendFlexFileLayoutBodyV2 encodes the ffv2_layout4 of
// draft-haynes-nfsv4-flex-filesv2-00 section 3.1: the stripe unit, the
// mirrors with their data servers, the flags and the stats hint. The
// device address is the version one ff_device_addr4, whose version
// array must carry one entry per stateid and per file handle.
func AppendFlexFileLayoutBodyV2(b []byte, flags uint32, stripeUnit uint64, mirrors []FlexMirrorV2) []byte {
b = xdr.AppendUint64(b, stripeUnit)
b = xdr.AppendUint32(b, uint32(len(mirrors)))
for _, m := range mirrors {
b = xdr.AppendUint32(b, uint32(len(m.DataServers)))
for _, ds := range m.DataServers {
b = append(b, ds.DeviceID[:]...)
b = xdr.AppendUint32(b, ds.Efficiency)
b = xdr.AppendUint32(b, uint32(len(ds.Stateids)))
for _, st := range ds.Stateids {
b = st.Append(b)
}
b = xdr.AppendUint32(b, uint32(len(ds.FHs)))
for _, fh := range ds.FHs {
b = xdr.AppendVarOpaque(b, fh)
}
b = xdr.AppendString(b, ds.User)
b = xdr.AppendString(b, ds.Group)
// opaque_auth: the flavour and the credential body.
b = xdr.AppendUint32(b, ds.AuthFlavor)
b = xdr.AppendVarOpaque(b, ds.AuthBody)
}
}
b = xdr.AppendUint32(b, flags)
return xdr.AppendUint32(b, 0) // stats collect hint
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The byte range locking layer of RFC 8881 sections 15.10 to 15.13 and
// 18.11 to 18.13: LOCK, LOCKT and LOCKU with their share of unions.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Lock types of the nfs_lock_type4 enum.
const (
LockTypeRead = 1
LockTypeWrite = 2
)
// AppendLockArgsNew encodes the LOCK4args for the first LOCK of a new
// lock owner: the locker union carries the open stateid the lock hangs
// from and the identity of the lock owner.
func AppendLockArgsNew(b []byte, openStateid Stateid, lockClientid uint64, lockOwner []byte, lockType uint32, reclaim bool, offset, length uint64) []byte {
b = xdr.AppendUint32(b, OpLock)
b = xdr.AppendUint32(b, lockType)
b = xdr.AppendBool(b, reclaim)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendBool(b, true) // new_lock_owner
b = xdr.AppendUint32(b, 0) // open_seqid, deprecated
b = openStateid.Append(b)
b = xdr.AppendUint32(b, 0) // lock_seqid, deprecated
b = xdr.AppendUint64(b, lockClientid)
return xdr.AppendVarOpaque(b, lockOwner)
}
// AppendLockArgsExisting encodes the LOCK4args for a lock owner that
// already holds a lock stateid.
func AppendLockArgsExisting(b []byte, lockStateid Stateid, lockType uint32, offset, length uint64) []byte {
b = xdr.AppendUint32(b, OpLock)
b = xdr.AppendUint32(b, lockType)
b = xdr.AppendBool(b, reclaimMarker)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendBool(b, false) // not a new lock owner
b = lockStateid.Append(b)
return xdr.AppendUint32(b, 0) // lock_seqid, deprecated
}
// reclaimMarker keeps the bool arguments of the locker union readable.
const reclaimMarker = false
// AppendLockuArgs encodes the LOCKU argop: the release of one range of a
// lock stateid.
func AppendLockuArgs(b []byte, lockStateid Stateid, offset, length uint64) []byte {
b = xdr.AppendUint32(b, OpLocku)
b = xdr.AppendUint32(b, LockTypeWrite) // the standard echoes a type; the stateid rules
b = xdr.AppendUint32(b, 0) // seqid, deprecated
b = lockStateid.Append(b)
b = xdr.AppendUint64(b, offset)
return xdr.AppendUint64(b, length)
}
// AppendLocktArgs encodes the LOCKT argop: a probe whether a lock of the
// type over the range would conflict, for the named owner.
func AppendLocktArgs(b []byte, lockType uint32, offset, length uint64, clientid uint64, owner []byte) []byte {
b = xdr.AppendUint32(b, OpLockt)
b = xdr.AppendUint32(b, lockType)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendUint64(b, clientid)
return xdr.AppendVarOpaque(b, owner)
}
// AppendLockRes encodes a successful LOCK4res: the lock stateid.
func AppendLockRes(b []byte, stateid Stateid) []byte {
return stateid.Append(b)
}
// AppendLockuRes encodes a successful LOCKU4res: the lock stateid, with
// its sequence bumped.
func AppendLockuRes(b []byte, stateid Stateid) []byte {
return stateid.Append(b)
}
// AppendLocktResOK appends a LOCKT result for the OK status, which carries
// nothing: no conflict was found.
func AppendLocktResOK(b []byte) []byte {
return b
}
// AppendLocktResDenied appends a LOCKT result body for the DENIED status:
// the conflicting lock, with its holder.
func AppendLocktResDenied(b []byte, offset, length uint64, lockType uint32, clientid uint64, owner []byte) []byte {
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendUint32(b, lockType)
b = xdr.AppendUint64(b, clientid)
return xdr.AppendVarOpaque(b, owner)
}
// A LockDenied is the conflicting lock a LOCKT answers.
type LockDenied struct {
Offset uint64
Length uint64
LockType uint32
ClientID uint64
Owner []byte
}
// DecodeLocktResBody decodes a LOCKT body. On the DENIED status it returns
// the conflicting lock; on OK the body is empty.
func DecodeLocktResBody(status uint32, body []byte) (LockDenied, error) {
if status != ErrDenied {
return LockDenied{}, nil
}
d := xdr.NewDecoder(body)
var denied LockDenied
var err error
if denied.Offset, err = d.Uint64(); err != nil {
return denied, err
}
if denied.Length, err = d.Uint64(); err != nil {
return denied, err
}
if denied.LockType, err = d.Uint32(); err != nil {
return denied, err
}
if denied.ClientID, err = d.Uint64(); err != nil {
return denied, err
}
denied.Owner, err = d.VarOpaque()
return denied, err
}
// resBodyLenLockt measures the LOCKT body: empty on OK, a full LOCK4denied
// on DENIED.
func resBodyLenLockt(status uint32, body []byte) (int, error) {
if status != ErrDenied {
return 0, nil
}
d := xdr.NewDecoder(body)
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.Uint32(); err != nil {
return 0, err
}
if _, err := d.Uint64(); err != nil {
return 0, err
}
if _, err := d.VarOpaque(); err != nil {
return 0, err
}
return len(body) - d.Remaining(), nil
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestLockuAndLocktResWire(t *testing.T) {
var st Stateid
st[4], st[5], st[6], st[7] = 'L', 'O', 'C', 'K'
arg := AppendLockuArgs(nil, st, 8, 32)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpLocku {
t.Fatalf("op: %d, %v", op, err)
}
if lt, err := d.Uint32(); err != nil || lt != LockTypeWrite {
t.Fatalf("locktype: %v", err)
}
if _, err := d.Uint32(); err != nil {
t.Fatal(err)
}
if raw, err := d.Raw(16); err != nil || raw[4] != 'L' {
t.Fatalf("stateid: %x, %v", raw, err)
}
if off, err := d.Uint64(); err != nil || off != 8 {
t.Fatalf("offset: %v", err)
}
if length, err := d.Uint64(); err != nil || length != 32 {
t.Fatalf("length: %v", err)
}
res := AppendLockuRes(nil, st)
if n, err := resBodyLen(OpLocku, ErrOK, res); err != nil || n != 16 {
t.Fatalf("locku measure: %d, %v", n, err)
}
okBody := AppendLocktResOK(nil)
if n, err := resBodyLen(OpLockt, ErrOK, okBody); err != nil || n != 0 {
t.Fatalf("lockt ok measure: %d, %v", n, err)
}
denied := AppendLocktResDenied(nil, 0, 100, LockTypeWrite, 0x1111, []byte("holder"))
if n, err := resBodyLen(OpLockt, ErrDenied, denied); err != nil || n != len(denied) {
t.Fatalf("lockt denied measure: %d of %d, %v", n, len(denied), err)
}
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The file system location attributes of RFC 5661 section 11.10: the
// fs_locations and fs_locations_info attributes that migration and
// referrals hand to the client.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// The attribute numbers of the location family.
const (
AttrFsLocations = 24 // fs_locations4
AttrFsLocationsInfo = 67 // fs_locations_info4
)
// The general flags of fli_flags, RFC 5661 section 11.10.1.
const (
Fsli4ifVarSub = 0x1
)
// The general flags of the fls_info GFLAGS byte.
const (
Fsli4gfWritable = 0x01
Fsli4gfCurReq = 0x02
Fsli4gfAbsent = 0x04
Fsli4gfGoing = 0x08
Fsli4gfSplit = 0x10
)
// An FsLocation is one fs_location4: the servers that share a root
// path.
type FsLocation struct {
Servers []string
RootPath []string
}
// An FsServer is one fs_locations_server4 replica entry: the currency
// hint, the opaque info bytes and the server name.
type FsServer struct {
Currency int32
Info []byte
Server string
}
// An FsItem is one fs_locations_item4: the replicas of one root path.
type FsItem struct {
Entries []FsServer
RootPath []string
}
// AppendPathname encodes a pathname4: an array of components.
func AppendPathname(b []byte, components []string) []byte {
b = xdr.AppendUint32(b, uint32(len(components)))
for _, c := range components {
b = xdr.AppendString(b, c)
}
return b
}
// AppendFsLocations encodes the fs_locations4 attribute.
func AppendFsLocations(b []byte, fsRoot []string, locations []FsLocation) []byte {
b = AppendPathname(b, fsRoot)
b = xdr.AppendUint32(b, uint32(len(locations)))
for _, l := range locations {
b = xdr.AppendUint32(b, uint32(len(l.Servers)))
for _, s := range l.Servers {
b = xdr.AppendString(b, s)
}
b = AppendPathname(b, l.RootPath)
}
return b
}
// AppendFsLocationsInfo encodes the fs_locations_info4 attribute: the
// flags, the validity window, the root and the replica items.
func AppendFsLocationsInfo(b []byte, flags uint32, validFor int32, fsRoot []string, items []FsItem) []byte {
b = xdr.AppendUint32(b, flags)
b = xdr.AppendInt32(b, validFor)
b = AppendPathname(b, fsRoot)
b = xdr.AppendUint32(b, uint32(len(items)))
for _, item := range items {
b = xdr.AppendUint32(b, uint32(len(item.Entries)))
for _, e := range item.Entries {
b = xdr.AppendInt32(b, e.Currency)
b = xdr.AppendVarOpaque(b, e.Info)
b = xdr.AppendString(b, e.Server)
}
b = AppendPathname(b, item.RootPath)
}
return b
}
// DecodeFsLocations reads the fs_locations4 attribute back.
func DecodeFsLocations(payload []byte) (fsRoot []string, locations []FsLocation, err error) {
d := xdr.NewDecoder(payload)
if fsRoot, err = decodePathname(d); err != nil {
return
}
var n uint32
if n, err = d.Uint32(); err != nil {
return
}
for i := uint32(0); i < n; i++ {
var l FsLocation
var count uint32
if count, err = d.Uint32(); err != nil {
return
}
for j := uint32(0); j < count; j++ {
var s string
if s, err = d.String(); err != nil {
return
}
l.Servers = append(l.Servers, s)
}
if l.RootPath, err = decodePathname(d); err != nil {
return
}
locations = append(locations, l)
}
return
}
func decodePathname(d *xdr.Decoder) ([]string, error) {
n, err := d.Uint32()
if err != nil {
return nil, err
}
var out []string
for range n {
s, err := d.String()
if err != nil {
return nil, err
}
out = append(out, s)
}
return out, nil
}
// AppendOpenattrArgs encodes the OPENATTR argop: the created flag.
func AppendOpenattrArgs(b []byte, created bool) []byte {
b = xdr.AppendUint32(b, OpOpenattr)
return xdr.AppendBool(b, created)
}
// AppendReadArgs encodes the READ argop.
func AppendReadArgs(b []byte, st Stateid, offset uint64, count uint32) []byte {
b = xdr.AppendUint32(b, OpRead)
b = st.Append(b)
b = xdr.AppendUint64(b, offset)
return xdr.AppendUint32(b, count)
}
+311
View File
@@ -0,0 +1,311 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// The location attributes round trip: the fs_locations attribute
// decodes back to the servers and root paths, and the info attribute
// carries the flags, the validity window and the replica entries.
func TestFsLocationsShapes(t *testing.T) {
blob := AppendFsLocations(nil, []string{"exports"}, []FsLocation{{
Servers: []string{"a.example", "b.example"},
RootPath: []string{"exports", "data"},
}})
root, locations, err := DecodeFsLocations(blob)
if err != nil {
t.Fatal(err)
}
if len(root) != 1 || root[0] != "exports" {
t.Fatalf("root %v", root)
}
if len(locations) != 1 || len(locations[0].Servers) != 2 ||
locations[0].Servers[1] != "b.example" ||
len(locations[0].RootPath) != 2 || locations[0].RootPath[1] != "data" {
t.Fatalf("locations %+v", locations)
}
// The fs_locations_info attribute: flags, validity, items with the
// opaque info bytes.
info := AppendFsLocationsInfo(nil, Fsli4ifVarSub, 60, []string{"exports"},
[]FsItem{{Entries: []FsServer{{
Currency: 3, Info: []byte{Fsli4gfWritable | Fsli4gfCurReq}, Server: "a.example",
}}, RootPath: []string{"data"}}})
d := xdr.NewDecoder(info)
if flags, err := d.Uint32(); err != nil || flags != Fsli4ifVarSub {
t.Fatalf("flags %d: %v", flags, err)
}
if valid, err := d.Int32(); err != nil || valid != 60 {
t.Fatalf("valid %d: %v", valid, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("root count %d: %v", n, err)
}
if rootComponent, err := d.String(); err != nil || rootComponent != "exports" {
t.Fatalf("root component %q: %v", rootComponent, err)
}
if _, err := d.Uint32(); err != nil { // items count
t.Fatal(err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("entries %d: %v", n, err)
}
if cur, err := d.Int32(); err != nil || cur != 3 {
t.Fatalf("currency %d: %v", cur, err)
}
if info2, err := d.VarOpaque(); err != nil || len(info2) != 1 ||
info2[0]&Fsli4gfWritable == 0 || info2[0]&Fsli4gfCurReq == 0 {
t.Fatalf("info % x: %v", info2, err)
}
if s, err := d.String(); err != nil || s != "a.example" {
t.Fatalf("server %q: %v", s, err)
}
// The item closes with the root path of the replicas.
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("rootpath count %d: %v", n, err)
}
if c, err := d.String(); err != nil || c != "data" {
t.Fatalf("rootpath %q: %v", c, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
}
// The emulated layout family bodies carry the device id first and the
// family payload after; the GETDEVICELIST pair round trips.
func TestLayoutFamilyShapes(t *testing.T) {
var dev [16]byte
copy(dev[:], "0123456789abcdef")
files := AppendFileLayoutBody(nil, dev, 4096, 2, 8,
[][]byte{[]byte("fh1"), []byte("fh2")})
d := xdr.NewDecoder(files)
if got, err := d.Raw(16); err != nil || !bytes.Equal(got, dev[:]) {
t.Fatalf("device % x: %v", got, err)
}
if u, err := d.Uint32(); err != nil || u != 4096 {
t.Fatalf("util %d: %v", u, err)
}
if fs, err := d.Uint32(); err != nil || fs != 2 {
t.Fatalf("first stripe %d: %v", fs, err)
}
if po, err := d.Uint64(); err != nil || po != 8 {
t.Fatalf("pattern %d: %v", po, err)
}
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("fh count %d: %v", n, err)
}
if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh1" {
t.Fatalf("fh %q: %v", fh, err)
}
if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh2" {
t.Fatalf("fh2 %q: %v", fh, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
// The device address: stripe indices and the multipath list.
addr := AppendFileDeviceAddr(nil, []uint32{0, 1},
[]NetAddr{{Netid: "tcp", Uaddr: "10.0.0.1.8.1"}})
d = xdr.NewDecoder(addr)
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("indices %d: %v", n, err)
}
if _, err := d.Raw(8); err != nil {
t.Fatal(err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("addrs %d: %v", n, err)
}
if id, err := d.String(); err != nil || id != "tcp" {
t.Fatalf("netid %q: %v", id, err)
}
if ua, err := d.String(); err != nil || ua != "10.0.0.1.8.1" {
t.Fatalf("uaddr %q: %v", ua, err)
}
// The block volume, the SCSI extent and the OSD component bodies
// carry their shapes.
block := AppendBlockDeviceAddr(nil, BlockVolume{DeviceID: dev, BaseOffset: 8, BlockCount: 64})
d = xdr.NewDecoder(block)
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("volumes %d: %v", n, err)
}
if typ, err := d.Uint32(); err != nil || typ != 1 {
t.Fatalf("volume type %d: %v", typ, err)
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, dev[:]) {
t.Fatalf("volume device % x: %v", raw, err)
}
if bo, err := d.Uint64(); err != nil || bo != 8 {
t.Fatalf("base offset %d: %v", bo, err)
}
if bc, err := d.Uint64(); err != nil || bc != 64 {
t.Fatalf("block count %d: %v", bc, err)
}
scsi := AppendScsiLayoutBody(nil, dev, 1, 2, 3)
d = xdr.NewDecoder(scsi)
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("extents %d: %v", n, err)
}
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, dev[:]) {
t.Fatalf("extent device % x: %v", raw, err)
}
for _, want := range []uint64{1, 2, 3} {
if v, err := d.Uint64(); err != nil || v != want {
t.Fatalf("extent field %d: %v", v, err)
}
}
obj := AppendObjectLayoutBody(nil, dev, ObjectLayout{NumComponents: 1,
StripeUnit: 4096, GroupWidth: 1, GroupDepth: 1, RefTagSize: 4})
d = xdr.NewDecoder(obj)
if alg, err := d.Uint32(); err != nil || alg != 0 {
t.Fatalf("raid algorithm %d: %v", alg, err)
}
if su, err := d.Uint64(); err != nil || su != 4096 {
t.Fatalf("stripe unit %d: %v", su, err)
}
// The GETDEVICELIST pair round trips.
list := AppendGetDeviceListArgs(nil, LayoutTypeFlexfiles, 8, 5, [8]byte{9})
d = xdr.NewDecoder(list[4:])
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("list type %d: %v", typ, err)
}
if md, err := d.Uint32(); err != nil || md != 8 {
t.Fatalf("max devices %d: %v", md, err)
}
if c, err := d.Uint64(); err != nil || c != 5 {
t.Fatalf("cookie %d: %v", c, err)
}
raw, err = d.Raw(8)
if err != nil || raw[0] != 9 {
t.Fatalf("verifier % x: %v", raw, err)
}
res := AppendGetDeviceListRes(nil, 5, [8]byte{9}, [][16]byte{dev, {1}}, true)
d = xdr.NewDecoder(res)
if c, err := d.Uint64(); err != nil || c != 5 {
t.Fatalf("res cookie %d: %v", c, err)
}
if _, err := d.Raw(8); err != nil {
t.Fatal(err)
}
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("devices %d: %v", n, err)
}
if _, err := d.Raw(32); err != nil {
t.Fatal(err)
}
if eof, err := d.Bool(); err != nil || !eof {
t.Fatalf("eof %v: %v", eof, err)
}
}
// The OPENATTR and READ argop encoders carry their shapes.
func TestOpenattrReadArgs(t *testing.T) {
oa := AppendOpenattrArgs(nil, true)
d := xdr.NewDecoder(oa[4:])
if created, err := d.Bool(); err != nil || !created {
t.Fatalf("created %v: %v", created, err)
}
ra := AppendReadArgs(nil, Stateid{7}, 99, 1024)
d = xdr.NewDecoder(ra[4:])
raw, err := d.Raw(16)
if err != nil || raw[0] != 7 {
t.Fatalf("stateid % x: %v", raw, err)
}
if off, err := d.Uint64(); err != nil || off != 99 {
t.Fatalf("offset %d: %v", off, err)
}
if cnt, err := d.Uint32(); err != nil || cnt != 1024 {
t.Fatalf("count %d: %v", cnt, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
}
// The flexfiles version 2 layout body of
// draft-haynes-nfsv4-flex-filesv2-00 round trips: the data server
// carries the stateid array, the file handle list and the RPC
// credential the draft adds.
func TestFlexFileLayoutV2Shape(t *testing.T) {
var dev [16]byte
copy(dev[:], "v2flexdeviceid!!")
st := Stateid{2, 'F', 'F', 'V', '2'}
body := AppendFlexFileLayoutBodyV2(nil, 3, 0, []FlexMirrorV2{{
DataServers: []FlexDataServerV2{{
DeviceID: dev,
Efficiency: 7,
Stateids: []Stateid{st},
FHs: [][]byte{[]byte("fh-v2")},
User: "petr",
Group: "nfs",
AuthFlavor: 0,
AuthBody: nil,
}},
}})
d := xdr.NewDecoder(body)
if su, err := d.Uint64(); err != nil || su != 0 {
t.Fatalf("stripe unit %d: %v", su, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("mirrors %d: %v", n, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("servers %d: %v", n, err)
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, dev[:]) {
t.Fatalf("device % x: %v", raw, err)
}
if eff, err := d.Uint32(); err != nil || eff != 7 {
t.Fatalf("efficiency %d: %v", eff, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("stateids %d: %v", n, err)
}
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if n, err := d.Uint32(); err != nil || n != 1 {
t.Fatalf("fhs %d: %v", n, err)
}
if fh, err := d.VarOpaque(); err != nil || string(fh) != "fh-v2" {
t.Fatalf("fh %q: %v", fh, err)
}
if u, err := d.String(); err != nil || u != "petr" {
t.Fatalf("user %q: %v", u, err)
}
if g, err := d.String(); err != nil || g != "nfs" {
t.Fatalf("group %q: %v", g, err)
}
if flavor, err := d.Uint32(); err != nil || flavor != 0 {
t.Fatalf("auth flavor %d: %v", flavor, err)
}
if authBody, err := d.VarOpaque(); err != nil || len(authBody) != 0 {
t.Fatalf("auth body % x: %v", authBody, err)
}
if flags, err := d.Uint32(); err != nil || flags != 3 {
t.Fatalf("flags %d: %v", flags, err)
}
if hint, err := d.Uint32(); err != nil || hint != 0 {
t.Fatalf("stats hint %d: %v", hint, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left", d.Remaining())
}
}
+860
View File
@@ -0,0 +1,860 @@
// 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")
}
}
+1328
View File
File diff suppressed because it is too large Load Diff
+118
View File
@@ -0,0 +1,118 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The codecs of the remaining NFSv4.1 operations: the parent lookups, the
// attribute comparisons, the lock owner release and the session binding
// family.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Connection direction and binding values of the CREATE_SESSION and
// BIND_CONN_TO_SESSION channel directory, RFC 8881 section 2.10.1.
const (
Cdfc4Fore = 1
Cdfc4Back = 2
Cdfc4ForeOrBoth = 3
Cdfc4BackOrBoth = 7
Cdfs4Fore = 1
Cdfs4Back = 2
Cdfs4Both = 3
Cdfs4Arrow = 4
)
// AppendLookupp appends the LOOKUPP argop, which carries no arguments.
func AppendLookupp(b []byte) []byte {
return xdr.AppendUint32(b, OpLookupp)
}
// AppendPutPubfh appends the PUTPUBFH argop, which carries no arguments.
func AppendPutPubfh(b []byte) []byte {
return xdr.AppendUint32(b, OpPutPubfh)
}
// AppendVerifyArgs appends the VERIFY argop: the attributes the file must
// carry for the compound to proceed. NVERIFY shares the shape and demands
// the opposite.
func AppendVerifyArgs(b []byte, request Bitmap, a Attrs) []byte {
b = xdr.AppendUint32(b, OpVerify)
return AppendFattr(b, request, a)
}
// AppendNverifyArgs appends the NVERIFY argop: the attributes the file
// must not carry.
func AppendNverifyArgs(b []byte, request Bitmap, a Attrs) []byte {
b = xdr.AppendUint32(b, OpNverify)
return AppendFattr(b, request, a)
}
// AppendReleaseLockOwnerArgs appends the RELEASE_LOCKOWNER argop: the
// client whose lock owner dies and the owner itself.
func AppendReleaseLockOwnerArgs(b []byte, clientid uint64, owner []byte) []byte {
b = xdr.AppendUint32(b, OpReleaseLockOwner)
b = xdr.AppendUint64(b, clientid)
return xdr.AppendVarOpaque(b, owner)
}
// AppendDelegReturnArgs appends the DELEGRETURN argop: the stateid of the
// delegation the client gives back.
func AppendDelegReturnArgs(b []byte, st Stateid) []byte {
b = xdr.AppendUint32(b, OpDelegReturn)
return st.Append(b)
}
// AppendBackchannelCtlArgs appends the BACKCHANNEL_CTL argop: the program
// number of the back channel the client runs.
func AppendBackchannelCtlArgs(b []byte, program uint32) []byte {
b = xdr.AppendUint32(b, OpBackchannelCtl)
return xdr.AppendUint32(b, program)
}
// AppendBindConnToSessionArgs appends the BIND_CONN_TO_SESSION argop of
// RFC 7863: the session, the direction the connection should serve and
// the RDMA mode flag.
func AppendBindConnToSessionArgs(b []byte, id SessionID, dir uint32, rdma bool) []byte {
b = xdr.AppendUint32(b, OpBindConnToSession)
b = append(b, id[:]...)
b = xdr.AppendUint32(b, dir)
return xdr.AppendBool(b, rdma)
}
// AppendBindConnToSessionRes appends the result: the session and the
// direction the server granted.
func AppendBindConnToSessionRes(b []byte, id SessionID, dir uint32) []byte {
b = append(b, id[:]...)
return xdr.AppendUint32(b, dir)
}
// AppendFreeStateidArgs appends the FREE_STATEID argop: the lock stateid
// the client retires.
func AppendFreeStateidArgs(b []byte, st Stateid) []byte {
b = xdr.AppendUint32(b, OpFreeStateid)
return st.Append(b)
}
// AppendTestStateidArgs appends the TEST_STATEID argop: the stateids the
// client asks about.
func AppendTestStateidArgs(b []byte, sts []Stateid) []byte {
b = xdr.AppendUint32(b, OpTestStateid)
b = xdr.AppendUint32(b, uint32(len(sts)))
for _, st := range sts {
b = st.Append(b)
}
return b
}
// AppendTestStateidRes appends the result: one status per stateid, in the
// order the arguments named them.
func AppendTestStateidRes(b []byte, statuses []uint32) []byte {
b = xdr.AppendUint32(b, uint32(len(statuses)))
for _, s := range statuses {
b = xdr.AppendUint32(b, s)
}
return b
}
+284
View File
@@ -0,0 +1,284 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The wire codecs of the NFSv4.2 operations, RFC 7862, with the shapes
// taken from the XDR of RFC 7863.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Content types of the data_content4 enum, RFC 7862 section 8.2.
const (
ContentData = 0
ContentHole = 1
)
// Error codes the copy family answers with, RFC 7862 section 15.1.
const (
ErrOffloadDenied = 10091
ErrOffloadNoReqs = 10094
NfsSyncNothing = 0
NfsSyncDataSync = 1
NfsSyncFileSync = 2
OffloadNotReq = 0
OffloadSuccess = 1
OffloadDenied = 2
OffloadFatal = 3
OffloadNoFallback = 4
)
// AppendSeekArgs encodes the SEEK argop: the stateid, the offset and the
// content kind the client searches for.
func AppendSeekArgs(b []byte, st Stateid, offset uint64, what uint32) []byte {
b = xdr.AppendUint32(b, OpSeek)
b = st.Append(b)
b = xdr.AppendUint64(b, offset)
return xdr.AppendUint32(b, what)
}
// AppendSeekRes encodes a successful SEEK result: the end of file flag
// and the offset of the next content of the requested kind.
func AppendSeekRes(b []byte, eof bool, offset uint64) []byte {
b = xdr.AppendBool(b, eof)
return xdr.AppendUint64(b, offset)
}
// AppendAllocateArgs encodes the ALLOCATE argop: the reserved range.
func AppendAllocateArgs(b []byte, st Stateid, offset, length uint64) []byte {
b = xdr.AppendUint32(b, OpAllocate)
b = st.Append(b)
b = xdr.AppendUint64(b, offset)
return xdr.AppendUint64(b, length)
}
// AppendDeallocateArgs encodes the DEALLOCATE argop: the released range.
func AppendDeallocateArgs(b []byte, st Stateid, offset, length uint64) []byte {
b = xdr.AppendUint32(b, OpDeallocate)
b = st.Append(b)
b = xdr.AppendUint64(b, offset)
return xdr.AppendUint64(b, length)
}
// AppendIoAdviseArgs encodes the IO_ADVISE argop: the range and the hint
// bitmap.
func AppendIoAdviseArgs(b []byte, st Stateid, offset, length uint64, hints Bitmap) []byte {
b = xdr.AppendUint32(b, OpIoAdvise)
b = st.Append(b)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
return hints.AppendTo(b)
}
// AppendIoAdviseRes encodes a successful IO_ADVISE result: the hints the
// server accepted.
func AppendIoAdviseRes(b []byte, hints Bitmap) []byte {
return hints.AppendTo(b)
}
// A CopySourceServer is one netloc4 of the COPY argument list: the
// servers the source lives on.
type CopySourceServer struct {
Type uint32
Name string
Addr NetAddr
}
// AppendCopyArgs encodes the COPY argop: the source and destination
// stateids, the ranges and the source server list.
func AppendCopyArgs(b []byte, src, dst Stateid, srcOff, dstOff, count uint64, consecutive, synchronous bool, sources []CopySourceServer) []byte {
b = xdr.AppendUint32(b, OpCopy)
b = src.Append(b)
b = dst.Append(b)
b = xdr.AppendUint64(b, srcOff)
b = xdr.AppendUint64(b, dstOff)
b = xdr.AppendUint64(b, count)
b = xdr.AppendBool(b, consecutive)
b = xdr.AppendBool(b, synchronous)
b = xdr.AppendUint32(b, uint32(len(sources)))
for _, s := range sources {
b = xdr.AppendUint32(b, s.Type)
switch s.Type {
case 1, 2:
b = xdr.AppendString(b, s.Name)
case 3:
b = xdr.AppendString(b, s.Addr.Netid)
b = xdr.AppendString(b, s.Addr.Uaddr)
}
}
return b
}
// AppendWriteResponse encodes the write_response4 the copy family
// answers with.
func AppendWriteResponse(b []byte, callbackSt Stateid, callbackSet bool, count uint64, committed uint32, verifier [8]byte) []byte {
if callbackSet {
b = xdr.AppendUint32(b, 1)
b = callbackSt.Append(b)
} else {
b = xdr.AppendUint32(b, 0)
}
b = xdr.AppendUint64(b, count)
b = xdr.AppendUint32(b, committed)
return append(b, verifier[:]...)
}
// AppendCopyRes encodes a successful COPY result: the write response and
// the requirements the server met.
func AppendCopyRes(b []byte, callbackSt Stateid, callbackSet bool, count uint64, committed uint32, verifier [8]byte, consecutive, synchronous bool) []byte {
b = AppendWriteResponse(b, callbackSt, callbackSet, count, committed, verifier)
b = xdr.AppendBool(b, consecutive)
return xdr.AppendBool(b, synchronous)
}
// AppendCopyNotifyArgs encodes the COPY_NOTIFY argop: the source stateid
// and the destination the client copies to.
func AppendCopyNotifyArgs(b []byte, st Stateid, dstType uint32, dstName string, dstAddr NetAddr) []byte {
b = xdr.AppendUint32(b, OpCopyNotify)
b = st.Append(b)
b = xdr.AppendUint32(b, dstType)
switch dstType {
case 1, 2:
b = xdr.AppendString(b, dstName)
case 3:
b = xdr.AppendString(b, dstAddr.Netid)
b = xdr.AppendString(b, dstAddr.Uaddr)
}
return b
}
// AppendCopyNotifyRes encodes a successful COPY_NOTIFY result: the lease
// the authorization lives, the stateid naming it and the source servers.
func AppendCopyNotifyRes(b []byte, leaseSeconds int64, st Stateid, sources []CopySourceServer) []byte {
b = xdr.AppendInt64(b, leaseSeconds)
b = xdr.AppendUint32(b, 0)
b = st.Append(b)
b = xdr.AppendUint32(b, uint32(len(sources)))
for _, s := range sources {
b = xdr.AppendUint32(b, s.Type)
switch s.Type {
case 1, 2:
b = xdr.AppendString(b, s.Name)
case 3:
b = xdr.AppendString(b, s.Addr.Netid)
b = xdr.AppendString(b, s.Addr.Uaddr)
}
}
return b
}
// AppendOffloadCancelArgs encodes the OFFLOAD_CANCEL argop: the stateid
// of the copy to abandon.
func AppendOffloadCancelArgs(b []byte, st Stateid) []byte {
b = xdr.AppendUint32(b, OpOffloadCancel)
return st.Append(b)
}
// AppendOffloadStatusArgs encodes the OFFLOAD_STATUS argop: the stateid
// of the copy in flight.
func AppendOffloadStatusArgs(b []byte, st Stateid) []byte {
b = xdr.AppendUint32(b, OpOffloadStatus)
return st.Append(b)
}
// AppendOffloadStatusRes encodes a successful OFFLOAD_STATUS result: the
// bytes copied so far and the completion code.
func AppendOffloadStatusRes(b []byte, count uint64, complete uint32) []byte {
b = xdr.AppendUint64(b, count)
return xdr.AppendUint32(b, complete)
}
// AppendCloneArgs encodes the CLONE argop: the ranges of the clone. The
// saved file handle is the source, the current one the destination.
func AppendCloneArgs(b []byte, src, dst Stateid, srcOff, dstOff, count uint64) []byte {
b = xdr.AppendUint32(b, OpClone)
b = src.Append(b)
b = dst.Append(b)
b = xdr.AppendUint64(b, srcOff)
b = xdr.AppendUint64(b, dstOff)
return xdr.AppendUint64(b, count)
}
// AppendLayoutErrorArgs encodes the LAYOUTERROR argop: the failing range
// and one error per device.
func AppendLayoutErrorArgs(b []byte, offset, length uint64, st Stateid, device [16]byte, status, opnum uint32) []byte {
b = xdr.AppendUint32(b, OpLayoutError)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = st.Append(b)
b = xdr.AppendUint32(b, 1)
b = append(b, device[:]...)
b = xdr.AppendUint32(b, status)
return xdr.AppendUint32(b, opnum)
}
// AppendLayoutStatsArgs encodes the LAYOUTSTATS argop: the reported range
// and the IO counters of the data server.
func AppendLayoutStatsArgs(b []byte, offset, length uint64, st Stateid, readCount, readBytes, writeCount, writeBytes uint64, device [16]byte, updateType uint32, updateBody []byte) []byte {
b = xdr.AppendUint32(b, OpLayoutStats)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = st.Append(b)
b = xdr.AppendUint64(b, readCount)
b = xdr.AppendUint64(b, readBytes)
b = xdr.AppendUint64(b, writeCount)
b = xdr.AppendUint64(b, writeBytes)
b = append(b, device[:]...)
b = xdr.AppendUint32(b, updateType)
return xdr.AppendVarOpaque(b, updateBody)
}
// AppendReadPlusArgs encodes the READ_PLUS argop: the range to read.
func AppendReadPlusArgs(b []byte, st Stateid, offset uint64, count uint32) []byte {
b = xdr.AppendUint32(b, OpReadPlus)
b = st.Append(b)
b = xdr.AppendUint64(b, offset)
return xdr.AppendUint32(b, count)
}
// AppendReadPlusDataRes encodes a successful READ_PLUS result that
// carries one data segment.
func AppendReadPlusDataRes(b []byte, eof bool, offset uint64, data []byte) []byte {
b = xdr.AppendBool(b, eof)
b = xdr.AppendUint32(b, 1)
b = xdr.AppendUint32(b, ContentData)
b = xdr.AppendUint64(b, offset)
return xdr.AppendVarOpaque(b, data)
}
// AppendReadPlusHoleRes encodes a successful READ_PLUS result that
// reports a hole.
func AppendReadPlusHoleRes(b []byte, eof bool, offset, length uint64) []byte {
b = xdr.AppendBool(b, eof)
b = xdr.AppendUint32(b, 1)
b = xdr.AppendUint32(b, ContentHole)
b = xdr.AppendUint64(b, offset)
return xdr.AppendUint64(b, length)
}
// AppendWriteSameArgs encodes the WRITE_SAME argop: the stateid, the
// stability and the application data block to write. The block number
// is a count4 on the wire, RFC 7862 section 15.8.
func AppendWriteSameArgs(b []byte, st Stateid, stable uint32, offset, blockSize, blockCount, relOffBlocknum uint64, blockNum uint32, relOffPattern uint64, pattern []byte) []byte {
b = xdr.AppendUint32(b, OpWriteSame)
b = st.Append(b)
b = xdr.AppendUint32(b, stable)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, blockSize)
b = xdr.AppendUint64(b, blockCount)
b = xdr.AppendUint64(b, relOffBlocknum)
b = xdr.AppendUint32(b, blockNum)
b = xdr.AppendUint64(b, relOffPattern)
return xdr.AppendVarOpaque(b, pattern)
}
// AppendWriteSameRes encodes a successful WRITE_SAME result: the write
// response alone.
func AppendWriteSameRes(b []byte, count uint64, committed uint32, verifier [8]byte) []byte {
b = xdr.AppendUint32(b, 0)
b = xdr.AppendUint64(b, count)
b = xdr.AppendUint32(b, committed)
return append(b, verifier[:]...)
}
+152
View File
@@ -0,0 +1,152 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestOps42Shapes(t *testing.T) {
t.Parallel()
st := Stateid{2, 'S'}
// SEEK args and res.
seek := AppendSeekArgs(nil, st, 8, ContentHole)
d := xdr.NewDecoder(seek[4:])
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if off, err := d.Uint64(); err != nil || off != 8 {
t.Fatalf("offset %d: %v", off, err)
}
if what, err := d.Uint32(); err != nil || what != ContentHole {
t.Fatalf("what %d: %v", what, err)
}
res := AppendSeekRes(nil, true, 4096)
if n, err := resBodyLen(OpSeek, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("seek res measured %d of %d: %v", n, len(res), err)
}
// COPY res measures through the write response and requirements.
verifier := [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
copyRes := AppendCopyRes(nil, st, false, 128, NfsSyncFileSync, verifier, true, true)
if n, err := resBodyLen(OpCopy, ErrOK, copyRes); err != nil || n != len(copyRes) {
t.Fatalf("copy res measured %d of %d: %v", n, len(copyRes), err)
}
// READ_PLUS res with one data segment.
rp := AppendReadPlusDataRes(nil, false, 0, []byte("data"))
if n, err := resBodyLen(OpReadPlus, ErrOK, rp); err != nil || n != len(rp) {
t.Fatalf("read plus res measured %d of %d: %v", n, len(rp), err)
}
rph := AppendReadPlusHoleRes(nil, true, 0, 4096)
if n, err := resBodyLen(OpReadPlus, ErrOK, rph); err != nil || n != len(rph) {
t.Fatalf("read plus hole res measured %d of %d: %v", n, len(rph), err)
}
// WRITE_SAME args decode through the adb fields. The block number
// is a count4 on the wire, RFC 7862.
ws := AppendWriteSameArgs(nil, st, StableFileSync, 8, 512, 2, 0, 0, 0, []byte("ab"))
d = xdr.NewDecoder(ws[4:])
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if stable, err := d.Uint32(); err != nil || stable != StableFileSync {
t.Fatalf("stable %d: %v", stable, err)
}
if off, err := d.Uint64(); err != nil || off != 8 {
t.Fatalf("offset %d: %v", off, err)
}
for _, want := range []uint64{512, 2, 0} {
if v, err := d.Uint64(); err != nil || v != want {
t.Fatalf("adb field %d: %v", v, err)
}
}
if bn, err := d.Uint32(); err != nil || bn != 0 {
t.Fatalf("adb block num %d: %v", bn, err)
}
if rp, err := d.Uint64(); err != nil || rp != 0 {
t.Fatalf("adb rel off pattern %d: %v", rp, err)
}
if p, err := d.VarOpaque(); err != nil || string(p) != "ab" {
t.Fatalf("pattern %q: %v", p, err)
}
wsr := AppendWriteSameRes(nil, 1024, NfsSyncFileSync, verifier)
if n, err := resBodyLen(OpWriteSame, ErrOK, wsr); err != nil || n != len(wsr) {
t.Fatalf("write same res measured %d of %d: %v", n, len(wsr), err)
}
// The small ops: allocate, io advise, offload status.
al := AppendAllocateArgs(nil, st, 0, 100)
d = xdr.NewDecoder(al[4:])
if _, err := d.Raw(16); err != nil {
t.Fatal(err)
}
if _, err := d.Uint64(); err != nil {
t.Fatal(err)
}
if l, err := d.Uint64(); err != nil || l != 100 {
t.Fatalf("length %d: %v", l, err)
}
hints := AppendIoAdviseRes(nil, Bitmap{}.With(2))
if n, err := resBodyLen(OpIoAdvise, ErrOK, hints); err != nil || n != len(hints) {
t.Fatalf("io advise res measured %d of %d: %v", n, len(hints), err)
}
os := AppendOffloadStatusRes(nil, 64, OffloadSuccess)
if n, err := resBodyLen(OpOffloadStatus, ErrOK, os); err != nil || n != len(os) {
t.Fatalf("offload status res measured %d of %d: %v", n, len(os), err)
}
clone := AppendCloneArgs(nil, st, st, 1, 2, 3)
d = xdr.NewDecoder(clone[4:])
if _, err := d.Raw(16); err != nil {
t.Fatal(err)
}
if _, err := d.Raw(16); err != nil {
t.Fatal(err)
}
for _, want := range []uint64{1, 2, 3} {
if v, err := d.Uint64(); err != nil || v != want {
t.Fatalf("clone field %d: %v", v, err)
}
}
}
func TestOps42RemainingShapes(t *testing.T) {
t.Parallel()
st := Stateid{4, 'C'}
// The remaining arg encoders: op code in front, fields in order.
bodies := map[string][]byte{
"deallocate": AppendDeallocateArgs(nil, st, 1, 2),
"ioadvise": AppendIoAdviseArgs(nil, st, 1, 2, Bitmap{}.With(1)),
"copy": AppendCopyArgs(nil, st, st, 1, 2, 3, true, false, nil),
"copynotify": AppendCopyNotifyArgs(nil, st, 3, "", NetAddr{Netid: "tcp", Uaddr: "1.2.3.4.8.1"}),
"cancel": AppendOffloadCancelArgs(nil, st),
"status": AppendOffloadStatusArgs(nil, st),
"layerr": AppendLayoutErrorArgs(nil, 1, 2, st, layoutTestDevice, ErrBadStateid, OpRead),
"laystats": AppendLayoutStatsArgs(nil, 1, 2, st, 1, 2, 3, 4, layoutTestDevice, 0, nil),
"readplus": AppendReadPlusArgs(nil, st, 1, 512),
}
for _, body := range bodies {
d := xdr.NewDecoder(body[4:])
if _, err := d.Raw(16); err != nil {
t.Fatal(err)
}
}
cn := AppendCopyNotifyRes(nil, 90, st, []CopySourceServer{{Type: 2, Name: "dst"}})
if n, err := resBodyLen(OpCopyNotify, ErrOK, cn); err != nil || n != len(cn) {
t.Fatalf("copy notify res measured %d of %d: %v", n, len(cn), err)
}
// A COPY result whose write response carries the callback stateid
// arm measures through the same shape.
wr := AppendCopyRes(nil, st, true, 10, NfsSyncDataSync, [8]byte{}, false, true)
if n, err := resBodyLen(OpCopy, ErrOK, wr); err != nil || n != len(wr) {
t.Fatalf("copy res with callback measured %d of %d: %v", n, len(wr), err)
}
}
var layoutTestDevice = [16]byte{1, 2, 3}
+319
View File
@@ -0,0 +1,319 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The pNFS layout layer: the wire codecs of LAYOUTGET, LAYOUTCOMMIT,
// LAYOUTRETURN and GETDEVICEINFO over the flexfiles layout of RFC 8435.
// The shapes follow the XDR of RFC 8881 sections 18.43 to 18.45 and RFC
// 8435 section 6.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Layout types of the layouttype4 enum, the registry values of RFC 7863
// and RFC 8435: files, objects and block volume are the NFSv4.1 core,
// flexfiles is four, SCSI five.
const (
LayoutTypeFiles = 1
LayoutTypeObjects = 2
LayoutTypeBlock = 3
LayoutTypeFlexfiles = 4
LayoutTypeScsi = 5
)
// Layout IO modes of the layoutiomode4 enum, RFC 7863.
const (
IoModeRead = 1
IoModeRW = 2
IoModeAny = 3
)
// LAYOUTCOMMIT uses the newtime4 union for the modify time a client
// asserts.
type NewTime struct {
Set bool
Seconds int64
Nseconds uint32
}
// AppendLayoutGetArgs encodes the whole LAYOUTGET argop: the signal flag,
// the layout type, the IO mode, the range, the stateid the layout hangs
// from and the response size budget.
func AppendLayoutGetArgs(b []byte, signalAvail bool, layoutType, iomode uint32, offset, length, minlength uint64, st Stateid, maxcount uint32) []byte {
b = xdr.AppendUint32(b, OpLayoutGet)
b = xdr.AppendBool(b, signalAvail)
b = xdr.AppendUint32(b, layoutType)
b = xdr.AppendUint32(b, iomode)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendUint64(b, minlength)
b = st.Append(b)
return xdr.AppendUint32(b, maxcount)
}
// A Layout4 is one segment of a granted layout: the byte range, the IO
// mode and the type specific body.
type Layout4 struct {
Offset uint64
Length uint64
IoMode uint32
Type uint32
Body []byte
}
// AppendLayout4 encodes one layout4.
func AppendLayout4(b []byte, l Layout4) []byte {
b = xdr.AppendUint64(b, l.Offset)
b = xdr.AppendUint64(b, l.Length)
b = xdr.AppendUint32(b, l.IoMode)
b = xdr.AppendUint32(b, l.Type)
return xdr.AppendVarOpaque(b, l.Body)
}
// AppendLayoutGetRes encodes a successful LAYOUTGET result: the layout
// stateid, the return-on-close flag and the layouts.
func AppendLayoutGetRes(b []byte, layoutSt Stateid, returnOnClose bool, layouts []Layout4) []byte {
b = layoutSt.Append(b)
b = xdr.AppendBool(b, returnOnClose)
b = xdr.AppendUint32(b, uint32(len(layouts)))
for _, l := range layouts {
b = AppendLayout4(b, l)
}
return b
}
// AppendLayoutCommitArgs encodes the LAYOUTCOMMIT argop body: the range,
// the reclaim flag, the layout stateid, the last write offset, the modify
// time and the layout update record.
func AppendLayoutCommitArgs(b []byte, offset, length uint64, reclaim bool, layoutSt Stateid, lastWriteSet bool, lastWrite uint64, timeSet bool, seconds int64, nseconds uint32, updateType uint32, updateBody []byte) []byte {
b = xdr.AppendUint32(b, OpLayoutCommit)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = xdr.AppendBool(b, reclaim)
b = layoutSt.Append(b)
if lastWriteSet {
b = xdr.AppendBool(b, true)
b = xdr.AppendUint64(b, lastWrite)
} else {
b = xdr.AppendBool(b, false)
}
if timeSet {
b = xdr.AppendBool(b, true)
b = xdr.AppendInt64(b, seconds)
b = xdr.AppendUint32(b, nseconds)
} else {
b = xdr.AppendBool(b, false)
}
b = xdr.AppendUint32(b, updateType)
return xdr.AppendVarOpaque(b, updateBody)
}
// AppendLayoutCommitRes encodes a successful LAYOUTCOMMIT result: the
// newsize4 union with the size the layout commit leaves the file at.
func AppendLayoutCommitRes(b []byte, newSize uint64) []byte {
b = xdr.AppendBool(b, true)
return xdr.AppendUint64(b, newSize)
}
// AppendLayoutReturnArgs encodes the LAYOUTRETURN argop of the whole file
// system or the whole client: no layoutreturn_file4 body follows.
func AppendLayoutReturnArgs(b []byte, reclaim bool, layoutType, iomode, returnKind uint32) []byte {
b = xdr.AppendUint32(b, OpLayoutReturn)
b = xdr.AppendBool(b, reclaim)
b = xdr.AppendUint32(b, layoutType)
b = xdr.AppendUint32(b, iomode)
return xdr.AppendUint32(b, returnKind)
}
// AppendLayoutReturnFileArgs encodes the LAYOUTRETURN argop of one file:
// the offset, the length and the layout stateid ride after the header,
// with the empty ffsid body of NFSv4.2.
func AppendLayoutReturnFileArgs(b []byte, reclaim bool, layoutType, iomode uint32, offset, length uint64, st Stateid) []byte {
b = AppendLayoutReturnArgs(b, reclaim, layoutType, iomode, ReturnFile)
b = xdr.AppendUint64(b, offset)
b = xdr.AppendUint64(b, length)
b = st.Append(b)
return xdr.AppendUint32(b, 0) // ffsid_info_type: the empty default
}
// AppendLayoutReturnRes encodes a successful LAYOUTRETURN result: the
// layoutreturn_stateid4 union with the returned flag set and the stateid
// of the layout as returned.
func AppendLayoutReturnRes(b []byte, stateid Stateid) []byte {
b = xdr.AppendBool(b, true)
return stateid.Append(b)
}
// AppendGetDeviceInfoArgs encodes the GETDEVICEINFO argop: the device id,
// the layout type, the response size budget and the notification types.
func AppendGetDeviceInfoArgs(b []byte, deviceID [16]byte, layoutType, maxcount uint32, notifyTypes Bitmap) []byte {
b = xdr.AppendUint32(b, OpGetDeviceInfo)
b = append(b, deviceID[:]...)
b = xdr.AppendUint32(b, layoutType)
b = xdr.AppendUint32(b, maxcount)
return notifyTypes.AppendTo(b)
}
// AppendGetDeviceInfoRes encodes a successful GETDEVICEINFO result: the
// device address of the flexfiles layout and the notification bitmap.
func AppendGetDeviceInfoRes(b []byte, addrBody []byte) []byte {
b = xdr.AppendUint32(b, LayoutTypeFlexfiles)
b = xdr.AppendVarOpaque(b, addrBody)
return Bitmap{}.AppendTo(b)
}
// A LayoutCommitArgs is the structured form of the LAYOUTCOMMIT argop.
type LayoutCommitArgs struct {
Offset uint64
Length uint64
Reclaim bool
LayoutSt Stateid
LastWriteSet bool
LastWrite uint64
TimeSet bool
Seconds int64
Nseconds uint32
UpdateType uint32
UpdateBody []byte
}
// Encode builds the whole LAYOUTCOMMIT argop.
func (a LayoutCommitArgs) Encode() []byte {
return AppendLayoutCommitArgs(nil, a.Offset, a.Length, a.Reclaim, a.LayoutSt,
a.LastWriteSet, a.LastWrite, a.TimeSet, a.Seconds, a.Nseconds, a.UpdateType, a.UpdateBody)
}
// A FlexDataServer is one ff_data_server4: the device the client reads
// and writes through, in the shape of RFC 8435 section 5.1.
type FlexDataServer struct {
DeviceID [16]byte
Efficiency uint32
Stateid Stateid
// FHs carries one file handle per file handle format the data server
// speaks; one entry is the common case.
FHs [][]byte
User string
Group string
}
// A FlexMirror is one ff_mirror4: a list of equivalent data servers that
// hold the same bytes.
type FlexMirror struct {
DataServers []FlexDataServer
}
// AppendFlexFileLayoutBody encodes the ff_layout4: the stripe unit, the
// mirrors with their data servers, the flags and the stats hint.
func AppendFlexFileLayoutBody(b []byte, flags uint32, stripeUnit uint64, mirrors []FlexMirror) []byte {
b = xdr.AppendUint64(b, stripeUnit)
b = xdr.AppendUint32(b, uint32(len(mirrors)))
for _, m := range mirrors {
b = xdr.AppendUint32(b, uint32(len(m.DataServers)))
for _, ds := range m.DataServers {
b = append(b, ds.DeviceID[:]...)
b = xdr.AppendUint32(b, ds.Efficiency)
b = ds.Stateid.Append(b)
b = xdr.AppendUint32(b, uint32(len(ds.FHs)))
for _, fh := range ds.FHs {
b = xdr.AppendVarOpaque(b, fh)
}
b = xdr.AppendString(b, ds.User)
b = xdr.AppendString(b, ds.Group)
}
}
b = xdr.AppendUint32(b, flags)
return xdr.AppendUint32(b, 0) // stats collect hint
}
// A FlexDeviceAddr is one ff_device_addr4: the net addresses of the data
// servers and the versions they speak.
type FlexDeviceAddr struct {
NetAddrs []NetAddr
Versions []FlexVersion
}
// A NetAddr is one netaddr4 of RFC 5661: a netid and a universal address.
type NetAddr struct {
Netid string
Uaddr string
}
// A FlexVersion is one ff_device_versions4: an NFS version a data server
// speaks and the transfer sizes it allows.
type FlexVersion struct {
Version uint32
MinorVersion uint32
RSize uint32
WSize uint32
TightlyBound bool
}
// AppendFlexDeviceAddr encodes the ff_device_addr4: the net addresses and
// the NFS versions the data servers speak.
func AppendFlexDeviceAddr(b []byte, d FlexDeviceAddr) []byte {
b = xdr.AppendUint32(b, uint32(len(d.NetAddrs)))
for _, na := range d.NetAddrs {
b = xdr.AppendString(b, na.Netid)
b = xdr.AppendString(b, na.Uaddr)
}
b = xdr.AppendUint32(b, uint32(len(d.Versions)))
for _, v := range d.Versions {
b = xdr.AppendUint32(b, v.Version)
b = xdr.AppendUint32(b, v.MinorVersion)
b = xdr.AppendUint32(b, v.RSize)
b = xdr.AppendUint32(b, v.WSize)
b = xdr.AppendBool(b, v.TightlyBound)
}
return b
}
// DecodeFlexDeviceAddr decodes an ff_device_addr4 body.
func DecodeFlexDeviceAddr(body []byte) (FlexDeviceAddr, error) {
d := xdr.NewDecoder(body)
var out FlexDeviceAddr
netCount, err := d.Uint32()
if err != nil {
return out, err
}
for range netCount {
netid, err := d.String()
if err != nil {
return out, err
}
uaddr, err := d.String()
if err != nil {
return out, err
}
out.NetAddrs = append(out.NetAddrs, NetAddr{Netid: netid, Uaddr: uaddr})
}
verCount, err := d.Uint32()
if err != nil {
return out, err
}
for range verCount {
v, err := d.Uint32()
if err != nil {
return out, err
}
minor, err := d.Uint32()
if err != nil {
return out, err
}
rsize, err := d.Uint32()
if err != nil {
return out, err
}
wsize, err := d.Uint32()
if err != nil {
return out, err
}
tight, err := d.Bool()
if err != nil {
return out, err
}
out.Versions = append(out.Versions, FlexVersion{Version: v, MinorVersion: minor, RSize: rsize, WSize: wsize, TightlyBound: tight})
}
return out, nil
}
+546
View File
@@ -0,0 +1,546 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// layoutOf decodes the single segment a test layout response carries.
func layoutOf(t *testing.T, body []byte) (Layout4, []byte) {
t.Helper()
d := xdr.NewDecoder(body)
var st Stateid
raw, err := d.Raw(16)
if err != nil {
t.Fatalf("layout stateid: %v", err)
}
copy(st[:], raw)
roc, err := d.Bool()
if err != nil || roc {
t.Fatalf("return on close: %v %v", roc, err)
}
n, err := d.Uint32()
if err != nil || n != 1 {
t.Fatalf("layout count %d: %v", n, err)
}
var l Layout4
if l.Offset, err = d.Uint64(); err != nil {
t.Fatal(err)
}
if l.Length, err = d.Uint64(); err != nil {
t.Fatal(err)
}
if l.IoMode, err = d.Uint32(); err != nil {
t.Fatal(err)
}
if l.Type, err = d.Uint32(); err != nil {
t.Fatal(err)
}
if l.Body, err = d.VarOpaque(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the layout response", d.Remaining())
}
return l, st[:]
}
func flexBodyOf(t *testing.T, body []byte) (FlexDataServer, uint64) {
t.Helper()
d := xdr.NewDecoder(body)
stripeUnit, err := d.Uint64()
if err != nil {
t.Fatal(err)
}
mirrors, err := d.Uint32()
if err != nil || mirrors != 1 {
t.Fatalf("mirror count %d: %v", mirrors, err)
}
servers, err := d.Uint32()
if err != nil || servers != 1 {
t.Fatalf("data server count %d: %v", servers, err)
}
var ds FlexDataServer
raw, rerr := d.Raw(16)
if rerr != nil {
t.Fatalf("device id: %v", rerr)
}
copy(ds.DeviceID[:], raw)
if ds.Efficiency, err = d.Uint32(); err != nil {
t.Fatal(err)
}
raw, rerr = d.Raw(16)
if rerr != nil {
t.Fatalf("data server stateid: %v", rerr)
}
copy(ds.Stateid[:], raw)
fhCount, ferr := d.Uint32()
if ferr != nil || fhCount != 1 {
t.Fatalf("file handle count %d: %v", fhCount, ferr)
}
var fhBody []byte
if fhBody, err = d.VarOpaque(); err != nil {
t.Fatal(err)
}
ds.FHs = [][]byte{fhBody}
if ds.User, err = d.String(); err != nil {
t.Fatal(err)
}
if ds.Group, err = d.String(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 8 {
t.Fatalf("%d bytes left in the flexfiles body", d.Remaining())
}
return ds, stripeUnit
}
func TestLayoutGetResBodyLen(t *testing.T) {
t.Parallel()
st := Stateid{1, 'L', 'A', 'Y', 1, 2, 3}
fh := []byte("filehandle")
body := AppendFlexFileLayoutBody(nil, 0, 4096, []FlexMirror{{
DataServers: []FlexDataServer{{DeviceID: [16]byte{9}, Stateid: st, FHs: [][]byte{fh}}},
}})
res := AppendLayoutGetRes(nil, st, false, []Layout4{{
Offset: 0, Length: ^uint64(0), IoMode: IoModeRW, Type: LayoutTypeFlexfiles, Body: body,
}})
n, err := resBodyLen(OpLayoutGet, ErrOK, res)
if err != nil {
t.Fatalf("resBodyLen: %v", err)
}
if n != len(res) {
t.Fatalf("measured %d, body is %d", n, len(res))
}
l, _ := layoutOf(t, res)
ds, stripe := flexBodyOf(t, l.Body)
if l.Type != LayoutTypeFlexfiles || l.IoMode != IoModeRW || l.Offset != 0 || l.Length != ^uint64(0) {
t.Fatalf("segment %+v", l)
}
if stripe != 4096 {
t.Fatalf("stripe unit %d", stripe)
}
if ds.DeviceID != [16]byte{9} || len(ds.FHs) != 1 || !bytes.Equal(ds.FHs[0], fh) || ds.Stateid != st {
t.Fatalf("data server %+v", ds)
}
}
func TestLayoutCommitReturnDeviceInfoResBodyLen(t *testing.T) {
t.Parallel()
st := Stateid{2, 'L', 'A', 'Y', 4, 5, 6}
commit := AppendLayoutCommitRes(nil, 101)
if n, err := resBodyLen(OpLayoutCommit, ErrOK, commit); err != nil || n != len(commit) {
t.Fatalf("commit res: %d %v", n, err)
}
ret := AppendLayoutReturnRes(nil, st)
if n, err := resBodyLen(OpLayoutReturn, ErrOK, ret); err != nil || n != len(ret) {
t.Fatalf("return res: %d %v", n, err)
}
addr := AppendFlexDeviceAddr(nil, FlexDeviceAddr{
NetAddrs: []NetAddr{{Netid: "tcp", Uaddr: "127.0.0.1.8.1"}},
Versions: []FlexVersion{{Version: 4, MinorVersion: 2, RSize: 1 << 20, WSize: 1 << 20}},
})
dev := AppendGetDeviceInfoRes(nil, addr)
n, err := resBodyLen(OpGetDeviceInfo, ErrOK, dev)
if err != nil {
t.Fatalf("device info res: %v", err)
}
if n != len(dev) {
t.Fatalf("measured %d, body is %d", n, len(dev))
}
d := xdr.NewDecoder(dev)
if typ, derr := d.Uint32(); derr != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("address type %d: %v", typ, derr)
}
blob, derr := d.VarOpaque()
if derr != nil {
t.Fatal(derr)
}
got, derr := DecodeFlexDeviceAddr(blob)
if derr != nil {
t.Fatalf("decode device address: %v", derr)
}
if len(got.NetAddrs) != 1 || got.NetAddrs[0].Netid != "tcp" || got.NetAddrs[0].Uaddr != "127.0.0.1.8.1" {
t.Fatalf("net addresses %+v", got.NetAddrs)
}
if len(got.Versions) != 1 || got.Versions[0] != (FlexVersion{Version: 4, MinorVersion: 2, RSize: 1 << 20, WSize: 1 << 20}) {
t.Fatalf("versions %+v", got.Versions)
}
if _, derr = ReadBitmap(d); derr != nil {
t.Fatal(derr)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the device info response", d.Remaining())
}
}
func TestLayoutArgShapes(t *testing.T) {
t.Parallel()
st := Stateid{7, 'L', 'A', 'Y', 8, 8, 8}
// LAYOUTGET args carry the op code and the full field set.
args := AppendLayoutGetArgs(nil, false, LayoutTypeFlexfiles, IoModeRW, 0, 100, 100, st, 1<<16)
if got := args[0:4]; !bytes.Equal(got, []byte{0, 0, 0, OpLayoutGet & 0xff}) {
t.Fatalf("layout get args open with % x", got)
}
d := xdr.NewDecoder(args[4:])
if _, err := d.Bool(); err != nil {
t.Fatal(err)
}
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("type %d: %v", typ, err)
}
if mode, err := d.Uint32(); err != nil || mode != IoModeRW {
t.Fatalf("iomode %d: %v", mode, err)
}
for range 3 {
if _, err := d.Uint64(); err != nil {
t.Fatal(err)
}
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if mc, err := d.Uint32(); err != nil || mc != 1<<16 {
t.Fatalf("maxcount %d: %v", mc, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the layout get args", d.Remaining())
}
// The file return carries the range, the stateid and the empty ffsid
// body; the whole client return carries nothing after the header.
fileArgs := AppendLayoutReturnFileArgs(nil, false, LayoutTypeFlexfiles, IoModeRW, 10, 90, st)
d = xdr.NewDecoder(fileArgs[4:])
reclaim, err := d.Bool()
if err != nil || reclaim {
t.Fatalf("reclaim %v: %v", reclaim, err)
}
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("type %d: %v", typ, err)
}
if mode, err := d.Uint32(); err != nil || mode != IoModeRW {
t.Fatalf("iomode %d: %v", mode, err)
}
if kind, err := d.Uint32(); err != nil || kind != ReturnFile {
t.Fatalf("return kind %d: %v", kind, err)
}
if off, err := d.Uint64(); err != nil || off != 10 {
t.Fatalf("offset %d: %v", off, err)
}
if length, err := d.Uint64(); err != nil || length != 90 {
t.Fatalf("length %d: %v", length, err)
}
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if ffsid, err := d.Uint32(); err != nil || ffsid != 0 {
t.Fatalf("ffsid type %d: %v", ffsid, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the file return", d.Remaining())
}
allArgs := AppendLayoutReturnArgs(nil, true, LayoutTypeFlexfiles, IoModeRW, ReturnAll)
d = xdr.NewDecoder(allArgs[4:])
if reclaim, err := d.Bool(); err != nil || !reclaim {
t.Fatalf("reclaim %v: %v", reclaim, err)
}
for range 3 {
if _, err := d.Uint32(); err != nil {
t.Fatal(err)
}
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the whole client return", d.Remaining())
}
}
func TestLayoutCommitArgsShape(t *testing.T) {
t.Parallel()
st := Stateid{3, 'L', 'A', 'Y', 9, 9, 9}
full := LayoutCommitArgs{
Offset: 8, Length: 92, Reclaim: true, LayoutSt: st,
LastWriteSet: true, LastWrite: 99,
TimeSet: true, Seconds: 1758000000, Nseconds: 500,
UpdateType: 1, UpdateBody: []byte("ff"),
}.Encode()
d := xdr.NewDecoder(full[4:])
if off, err := d.Uint64(); err != nil || off != 8 {
t.Fatalf("offset %d: %v", off, err)
}
if length, err := d.Uint64(); err != nil || length != 92 {
t.Fatalf("length %d: %v", length, err)
}
if reclaim, err := d.Bool(); err != nil || !reclaim {
t.Fatalf("reclaim %v: %v", reclaim, err)
}
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("stateid % x: %v", raw, err)
}
if set, err := d.Bool(); err != nil || !set {
t.Fatalf("last write set %v: %v", set, err)
}
if lw, err := d.Uint64(); err != nil || lw != 99 {
t.Fatalf("last write %d: %v", lw, err)
}
if ts, err := d.Bool(); err != nil || !ts {
t.Fatalf("time set %v: %v", ts, err)
}
if sec, err := d.Int64(); err != nil || sec != 1758000000 {
t.Fatalf("seconds %d: %v", sec, err)
}
if ns, err := d.Uint32(); err != nil || ns != 500 {
t.Fatalf("nseconds %d: %v", ns, err)
}
if ut, err := d.Uint32(); err != nil || ut != 1 {
t.Fatalf("update type %d: %v", ut, err)
}
if body, err := d.VarOpaque(); err != nil || !bytes.Equal(body, []byte("ff")) {
t.Fatalf("update body %q: %v", body, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the commit args", d.Remaining())
}
// The empty form: no last write, no time, no update body.
empty := LayoutCommitArgs{LayoutSt: st}.Encode()
d = xdr.NewDecoder(empty[4:])
for range 2 {
if _, err := d.Uint64(); err != nil {
t.Fatal(err)
}
}
if _, err := d.Bool(); err != nil {
t.Fatal(err)
}
if _, err := d.Raw(16); err != nil {
t.Fatal(err)
}
if set, err := d.Bool(); err != nil || set {
t.Fatalf("empty last write %v: %v", set, err)
}
if ts, err := d.Bool(); err != nil || ts {
t.Fatalf("empty time %v: %v", ts, err)
}
if _, err := d.Uint32(); err != nil {
t.Fatal(err)
}
if body, err := d.VarOpaque(); err != nil || len(body) != 0 {
t.Fatalf("empty update body %q: %v", body, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the empty commit args", d.Remaining())
}
}
func TestGetDeviceInfoArgsShape(t *testing.T) {
t.Parallel()
var device [16]byte
copy(device[:], "0123456789abcdef")
args := AppendGetDeviceInfoArgs(nil, device, LayoutTypeFlexfiles, 1<<16, Bitmap{}.With(0))
d := xdr.NewDecoder(args[4:])
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, device[:]) {
t.Fatalf("device id % x: %v", raw, err)
}
if typ, err := d.Uint32(); err != nil || typ != LayoutTypeFlexfiles {
t.Fatalf("type %d: %v", typ, err)
}
if mc, err := d.Uint32(); err != nil || mc != 1<<16 {
t.Fatalf("maxcount %d: %v", mc, err)
}
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the device info args", d.Remaining())
}
}
func TestOps41Shapes(t *testing.T) {
t.Parallel()
// VERIFY args are a bare fattr4 with the op in front.
args := AppendVerifyArgs(nil, OfBits(AttrMode), Attrs{Mode: 0o644})
d := xdr.NewDecoder(args[4:])
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if _, err := d.VarOpaque(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the verify args", d.Remaining())
}
// BIND_CONN_TO_SESSION echoes the session and the direction; its
// result body measures to exactly 20 bytes.
sid := SessionID{1, 2, 3}
res := AppendBindConnToSessionRes(nil, sid, Cdfs4Both)
if n, err := resBodyLen(OpBindConnToSession, ErrOK, res); err != nil || n != len(res) || n != 20 {
t.Fatalf("bind res measured %d of %d: %v", n, len(res), err)
}
d = xdr.NewDecoder(res)
raw, err := d.Raw(16)
if err != nil || !bytes.Equal(raw, sid[:]) {
t.Fatalf("session % x: %v", raw, err)
}
if dir, err := d.Uint32(); err != nil || dir != Cdfs4Both {
t.Fatalf("dir %d: %v", dir, err)
}
// TEST_STATEID measures to the status array.
statuses := AppendTestStateidRes(nil, []uint32{ErrOK, ErrBadStateid})
if n, err := resBodyLen(OpTestStateid, ErrOK, statuses); err != nil || n != len(statuses) {
t.Fatalf("test stateid res measured %d of %d: %v", n, len(statuses), err)
}
// RELEASE_LOCKOWNER and DELEGRETURN carry their shapes.
own := AppendReleaseLockOwnerArgs(nil, 0x99, []byte("owner"))
d = xdr.NewDecoder(own[4:])
if cid, err := d.Uint64(); err != nil || cid != 0x99 {
t.Fatalf("clientid %d: %v", cid, err)
}
if o, err := d.VarOpaque(); err != nil || string(o) != "owner" {
t.Fatalf("owner %q: %v", o, err)
}
st := Stateid{1, 'D', 'E', 'L', 7}
dr := AppendDelegReturnArgs(nil, st)
d = xdr.NewDecoder(dr[4:])
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("deleg return stateid % x: %v", raw, err)
}
fs := AppendFreeStateidArgs(nil, st)
d = xdr.NewDecoder(fs[4:])
raw, err = d.Raw(16)
if err != nil || !bytes.Equal(raw, st[:]) {
t.Fatalf("free stateid % x: %v", raw, err)
}
bc := AppendBackchannelCtlArgs(nil, 0x40000000)
d = xdr.NewDecoder(bc[4:])
if p, err := d.Uint32(); err != nil || p != 0x40000000 {
t.Fatalf("program %d: %v", p, err)
}
ts := AppendTestStateidArgs(nil, []Stateid{st, {}})
d = xdr.NewDecoder(ts[4:])
if n, err := d.Uint32(); err != nil || n != 2 {
t.Fatalf("count %d: %v", n, err)
}
for range 2 {
raw, err = d.Raw(16)
if err != nil {
t.Fatal(err)
}
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the test stateid args", d.Remaining())
}
}
func TestXattrShapes(t *testing.T) {
t.Parallel()
// GETXATTR args carry the name alone, RFC 8276: the operation
// number, the length word and the twelve byte name.
args := AppendGetXattrArgs(nil, "user.comment")
if len(args) != 20 {
t.Fatalf("getxattr args of %d bytes, want 20", len(args))
}
d := xdr.NewDecoder(args[4:])
if name, err := d.String(); err != nil || name != "user.comment" {
t.Fatalf("name %q: %v", name, err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the getxattr args", d.Remaining())
}
res := AppendGetXattrRes(nil, []byte("hello"))
n, err := resBodyLen(OpGetxattr, ErrOK, res)
if err != nil || n != len(res) {
t.Fatalf("getxattr res measured %d of %d: %v", n, len(res), err)
}
if v, err := DecodeGetXattrRes(res); err != nil || string(v) != "hello" {
t.Fatalf("getxattr value %q: %v", v, err)
}
// LISTXATTR res: the cookie, the names and the eof flag.
list := AppendListXattrRes(nil, 7, []string{"user.a", "user.b"}, false)
n, err = resBodyLen(OpListxattr, ErrOK, list)
if err != nil || n != len(list) {
t.Fatalf("listxattr res measured %d of %d: %v", n, len(list), err)
}
// SETXATTR args: the mode first, then the key and the value.
sx := AppendSetXattrArgs(nil, SetXattrCreate, "user.a", []byte("v"))
d = xdr.NewDecoder(sx[4:])
if mode, err := d.Uint32(); err != nil || mode != SetXattrCreate {
t.Fatalf("mode %d: %v", mode, err)
}
if name, err := d.String(); err != nil || name != "user.a" {
t.Fatalf("set name %q: %v", name, err)
}
if v, err := d.VarOpaque(); err != nil || string(v) != "v" {
t.Fatalf("value %q: %v", v, err)
}
rx := AppendRemoveXattrArgs(nil, "user.a")
d = xdr.NewDecoder(rx[4:])
if name, err := d.String(); err != nil || name != "user.a" {
t.Fatalf("remove name %q: %v", name, err)
}
}
func TestOps41RemainingShapes(t *testing.T) {
t.Parallel()
// The argument-less ops carry exactly the op code.
if got := AppendLookupp(nil); !bytes.Equal(got, []byte{0, 0, 0, OpLookupp & 0xff}) {
t.Fatalf("lookupp % x", got)
}
if got := AppendPutPubfh(nil); !bytes.Equal(got, []byte{0, 0, 0, OpPutPubfh & 0xff}) {
t.Fatalf("putpubfh % x", got)
}
// NVERIFY shares the VERIFY shape.
nv := AppendNverifyArgs(nil, OfBits(AttrSize), Attrs{Size: 9})
d := xdr.NewDecoder(nv[4:])
if _, err := ReadBitmap(d); err != nil {
t.Fatal(err)
}
if _, err := d.VarOpaque(); err != nil {
t.Fatal(err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the nverify args", d.Remaining())
}
// BIND_CONN_TO_SESSION args: the session, the direction and the
// RDMA mode flag.
bind := AppendBindConnToSessionArgs(nil, SessionID{9}, Cdfc4Back, true)
d = xdr.NewDecoder(bind[4:])
raw, err := d.Raw(16)
if err != nil || raw[0] != 9 {
t.Fatalf("session % x: %v", raw, err)
}
if dir, err := d.Uint32(); err != nil || dir != Cdfc4Back {
t.Fatalf("dir %d: %v", dir, err)
}
if rdma, err := d.Bool(); err != nil || !rdma {
t.Fatalf("rdma %v: %v", rdma, err)
}
// LISTXATTR args carry the cookie and the budget.
la := AppendListXattrArgs(nil, 3, 4096)
d = xdr.NewDecoder(la[4:])
if ck, err := d.Uint64(); err != nil || ck != 3 {
t.Fatalf("list cookie %d: %v", ck, err)
}
if mc, err := d.Uint32(); err != nil || mc != 4096 {
t.Fatalf("list maxcount %d: %v", mc, err)
}
}
+423
View File
@@ -0,0 +1,423 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The session establishment layer of RFC 8881 sections 18.35 to 18.37 and
// 18.46: EXCHANGE_ID names the client, CREATE_SESSION makes the session
// with its slot table, SEQUENCE drives the slots, DESTROY_SESSION tears
// one down.
package nfs4
import (
"encoding/binary"
"errors"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// EXCHGID4_FLAG values, RFC 8881 section 18.35.
const (
ExchgIDSuppMovedRefer = 0x00000001
ExchgIDSuppMovedMigr = 0x00000002
ExchgIDSuppFenceOps = 0x00000004
ExchgIDBindPrincStateid = 0x00000100
ExchgIDUseNonPnfs = 0x00010000
ExchgIDUsePnfsMds = 0x00020000
ExchgIDUsePnfsDs = 0x00040000
ExchgIDConfirmedR = 0x80000000
)
// CREATE_SESSION4_FLAG values.
const (
CreateSessionFlagPersist = 0x00000001
CreateSessionFlagConnBackChan = 0x00000002
CreateSessionFlagConnRdma = 0x00000004
)
// state protection choices of the EXCHANGE_ID unions, the
// state_protect_how4 enumeration of RFC 8881 section 18.35. Only the
// plain SP4_NONE is spoken by this build.
const (
StateProtectNone = 0 // SP4_NONE
StateProtectMachCred = 1 // SP4_MACH_CRED
StateProtectSSV = 2 // SP4_SSV
)
// A SessionID is the 16 byte session identifier RFC 7863 fixes
// (NFS4_SESSIONID_SIZE): an 8 byte server prefix followed by the 8 byte
// client id, both chosen by the server.
type SessionID [16]byte
// ClientIDOf splits the session id into its client id half.
func (s SessionID) ClientIDOf() uint64 {
return binary.BigEndian.Uint64(s[8:16])
}
// MakeSessionID builds a session id from the server prefix and the client
// id.
func MakeSessionID(prefix [8]byte, clientid uint64) SessionID {
var s SessionID
copy(s[:8], prefix[:])
binary.BigEndian.PutUint64(s[8:], clientid)
return s
}
// MakeNumberedSessionID builds a session id whose server half carries a
// per-session number, so every CREATE_SESSION mints a distinct id as
// RFC 8881 section 18.36 requires: four prefix bytes, the number and
// the client id.
func MakeNumberedSessionID(prefix [4]byte, number uint32, clientid uint64) SessionID {
var s SessionID
copy(s[:4], prefix[:])
binary.BigEndian.PutUint32(s[4:], number)
binary.BigEndian.PutUint64(s[8:], clientid)
return s
}
// A ChannelAttrs is the channel_attrs4 of a session: the sizes and limits
// of one connection direction.
type ChannelAttrs struct {
HeaderPad uint32
MaxRequest uint32
MaxResponse uint32
MaxRespResourced uint32
MaxOperations uint32
MaxRequests uint32
RdmaIRDEnabled bool
RdmaIRSizes []uint32
}
// DefaultForeChannel is the fore channel this server grants: enough slots
// for a real client, bounded to what one connection streams.
var DefaultForeChannel = ChannelAttrs{
MaxRequest: 1 << 22,
MaxResponse: 1 << 22,
MaxRespResourced: 1 << 22,
MaxOperations: 16,
MaxRequests: 8,
}
// DefaultBackChannel declares the minimal back channel: one operation and
// one request, with no channel attributes, which the client reads as
// nothing offered.
var DefaultBackChannel = ChannelAttrs{MaxRequest: 1 << 20, MaxResponse: 1 << 20, MaxRespResourced: 1 << 20, MaxOperations: 1, MaxRequests: 1}
// ErrOpMisordered marks a SEQUENCE that arrived on a slot with a sequence
// that is neither a retry nor the next one.
var ErrOpMisordered = errors.New("nfs4: sequence misordered")
// ExchangeIDArgs is the decoded EXCHANGE_ID4args.
type ExchangeIDArgs struct {
Verifier [8]byte
OwnerID []byte
Flags uint32
Protect uint32
}
// AppendExchangeIDArgs encodes the EXCHANGE_ID4args. The implementation id
// array is sent empty.
func AppendExchangeIDArgs(b []byte, verifier [8]byte, ownerID []byte, flags uint32) []byte {
b = xdr.AppendUint32(b, OpExchangeID)
b = append(b, verifier[:]...)
b = xdr.AppendVarOpaque(b, ownerID)
b = xdr.AppendUint32(b, flags)
b = xdr.AppendUint32(b, StateProtectNone)
return xdr.AppendUint32(b, 0) // empty eia_client_impl_id
}
// DecodeExchangeIDArgs decodes the EXCHANGE_ID4args from the decoder,
// which is positioned after the operation number.
func DecodeExchangeIDArgs(d *xdr.Decoder) (ExchangeIDArgs, error) {
var a ExchangeIDArgs
verf, err := d.Raw(8)
if err != nil {
return a, err
}
copy(a.Verifier[:], verf)
if a.OwnerID, err = d.VarOpaque(); err != nil {
return a, err
}
if a.Flags, err = d.Uint32(); err != nil {
return a, err
}
if a.Protect, err = d.Uint32(); err != nil {
return a, err
}
if a.Protect != StateProtectNone {
return a, ErrStateProtectNotSupp
}
// The client implementation id array is walked and ignored.
n, err := d.Uint32()
if err != nil {
return a, err
}
for range n {
if _, err = d.String(); err != nil {
return a, err
}
if _, err = d.String(); err != nil {
return a, err
}
if _, err = d.Uint64(); err != nil {
return a, err
}
if _, err = d.Uint32(); err != nil {
return a, err
}
}
return a, nil
}
// AppendExchangeIDRes encodes a successful EXCHANGE_ID4res: the client id,
// the create session sequence, the flags, the plain state protection, the
// server owner, the server scope and one implementation id entry, RFC 8881
// section 18.35. The field order and the entry shape follow what real
// clients decode: the minor id is a uint64 and the major id a bounded
// opaque, the scope follows the owner, and the entries carry no dummy.
func AppendExchangeIDRes(b []byte, clientid uint64, sequence, flags uint32, majorID []byte) []byte {
b = xdr.AppendUint64(b, clientid)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, flags)
b = xdr.AppendUint32(b, StateProtectNone)
// eir_server_owner: the major id stays constant across restarts so
// the client recognises the server.
b = xdr.AppendUint64(b, 0)
b = xdr.AppendVarOpaque(b, majorID)
// eir_server_scope: the servers that share this one's state. The
// Linux client fails the exchange when the field is missing.
b = xdr.AppendVarOpaque(b, majorID)
// eir_server_impl_id: one entry of domain, name and the zeroed date.
b = xdr.AppendUint32(b, 1)
b = xdr.AppendString(b, "sourcedock.dev")
b = xdr.AppendString(b, "nfsd")
b = xdr.AppendInt64(b, 0)
b = xdr.AppendUint32(b, 0)
return b
}
// AppendSessionChannel encodes a channel_attrs4.
func AppendSessionChannel(b []byte, c ChannelAttrs) []byte {
b = xdr.AppendUint32(b, c.HeaderPad)
b = xdr.AppendUint32(b, c.MaxRequest)
b = xdr.AppendUint32(b, c.MaxResponse)
b = xdr.AppendUint32(b, c.MaxRespResourced)
b = xdr.AppendUint32(b, c.MaxOperations)
b = xdr.AppendUint32(b, c.MaxRequests)
if c.RdmaIRDEnabled {
b = xdr.AppendUint32(b, uint32(len(c.RdmaIRSizes)))
for _, v := range c.RdmaIRSizes {
b = xdr.AppendUint32(b, v)
}
return b
}
// ca_rdma_ird is a counted array, RFC 8881 section 18.36: an empty
// one counts zero. Every real client decoder rejects anything else;
// the Linux client answers EINVAL for a count above one.
return xdr.AppendUint32(b, 0)
}
// DecodeSessionChannel decodes a channel_attrs4 body.
func DecodeSessionChannel(d *xdr.Decoder) (ChannelAttrs, error) {
var c ChannelAttrs
var err error
if c.HeaderPad, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxRequest, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxResponse, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxRespResourced, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxOperations, err = d.Uint32(); err != nil {
return c, err
}
if c.MaxRequests, err = d.Uint32(); err != nil {
return c, err
}
count, err := d.Uint32()
if err != nil {
return c, err
}
// ca_rdma_ird is a counted array: zero means no RDMA IRD support.
if count > 0 {
c.RdmaIRDEnabled = true
for range count {
v, verr := d.Uint32()
if verr != nil {
return c, verr
}
c.RdmaIRSizes = append(c.RdmaIRSizes, v)
}
}
return c, nil
}
// AppendCreateSessionArgs encodes the CREATE_SESSION4args.
func AppendCreateSessionArgs(b []byte, clientid uint64, sequence, flags uint32, fore, back ChannelAttrs, cbProgram uint32) []byte {
b = xdr.AppendUint32(b, OpCreateSession)
b = xdr.AppendUint64(b, clientid)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, flags)
b = AppendSessionChannel(b, fore)
b = AppendSessionChannel(b, back)
return xdr.AppendUint32(b, cbProgram)
}
// A CreateSessionArgs is the decoded CREATE_SESSION4args.
type CreateSessionArgs struct {
ClientID uint64
Sequence uint32
Flags uint32
Fore ChannelAttrs
Back ChannelAttrs
CBProgram uint32
}
// DecodeCreateSessionArgs decodes the CREATE_SESSION4args from the
// decoder, which is positioned after the operation number.
func DecodeCreateSessionArgs(d *xdr.Decoder) (CreateSessionArgs, error) {
var a CreateSessionArgs
var err error
if a.ClientID, err = d.Uint64(); err != nil {
return a, err
}
if a.Sequence, err = d.Uint32(); err != nil {
return a, err
}
if a.Flags, err = d.Uint32(); err != nil {
return a, err
}
if a.Fore, err = DecodeSessionChannel(d); err != nil {
return a, err
}
if a.Back, err = DecodeSessionChannel(d); err != nil {
return a, err
}
if a.CBProgram, err = d.Uint32(); err != nil {
return a, err
}
return a, nil
}
// AppendCreateSessionRes encodes a successful CREATE_SESSION4res.
func AppendCreateSessionRes(b []byte, id SessionID, sequence, flags uint32, fore, back ChannelAttrs) []byte {
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, flags)
b = AppendSessionChannel(b, fore)
return AppendSessionChannel(b, back)
}
// AppendDestroySessionArgs appends the DESTROY_SESSION argop.
func AppendDestroySessionArgs(b []byte, id SessionID) []byte {
b = xdr.AppendUint32(b, OpDestroySession)
return append(b, id[:]...)
}
// AppendSequenceArgs appends the SEQUENCE argop.
func AppendSequenceArgs(b []byte, id SessionID, sequence, slot, highestSlot uint32, cacheThis bool) []byte {
b = xdr.AppendUint32(b, OpSequence)
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, slot)
b = xdr.AppendUint32(b, highestSlot)
return xdr.AppendBool(b, cacheThis)
}
// A SequenceArgs is the decoded SEQUENCE4args.
type SequenceArgs struct {
SessionID SessionID
Sequence uint32
Slot uint32
HighestSlot uint32
CacheThis bool
}
// DecodeSequenceArgs decodes the SEQUENCE4args from the decoder, which
// is positioned after the operation number.
func DecodeSequenceArgs(d *xdr.Decoder) (SequenceArgs, error) {
var a SequenceArgs
raw, err := d.Raw(16)
if err != nil {
return a, err
}
copy(a.SessionID[:], raw)
if a.Sequence, err = d.Uint32(); err != nil {
return a, err
}
if a.Slot, err = d.Uint32(); err != nil {
return a, err
}
if a.HighestSlot, err = d.Uint32(); err != nil {
return a, err
}
if a.CacheThis, err = d.Bool(); err != nil {
return a, err
}
return a, nil
}
// NegotiateChannel clamps the requested channel attributes to the
// server's limits, RFC 8881 section 18.36: every negotiated value is the
// smaller of the request and the limit. A zero request carries meaning,
// ca_maxresponsesize_cached of zero above all, so it is answered with
// zero. Real clients reject a reply that exceeds their request.
func NegotiateChannel(requested ChannelAttrs, limits ChannelAttrs) ChannelAttrs {
nv := func(requested, limit uint32) uint32 {
if requested > limit {
return limit
}
return requested
}
return ChannelAttrs{
MaxRequest: nv(requested.MaxRequest, limits.MaxRequest),
MaxResponse: nv(requested.MaxResponse, limits.MaxResponse),
MaxRespResourced: nv(requested.MaxRespResourced, limits.MaxRespResourced),
MaxOperations: nv(requested.MaxOperations, limits.MaxOperations),
MaxRequests: nv(requested.MaxRequests, limits.MaxRequests),
}
}
// AppendSequenceRes encodes the successful SEQUENCE4resok of RFC 7863:
// the session id, the echoed identifiers and the status flags. The
// target highest slot mirrors the highest slot this build grants; a
// caller with its own view passes it instead.
func AppendSequenceRes(b []byte, id SessionID, sequence, slot, highestSlot, statusFlags uint32) []byte {
b = append(b, id[:]...)
b = xdr.AppendUint32(b, sequence)
b = xdr.AppendUint32(b, slot)
b = xdr.AppendUint32(b, highestSlot)
b = xdr.AppendUint32(b, highestSlot) // sr_target_highest_slotid
return xdr.AppendUint32(b, statusFlags)
}
// DecodeSequenceRes decodes the successful SEQUENCE4resok.
func DecodeSequenceRes(d *xdr.Decoder) (id SessionID, sequence, slot, highestSlot, targetHighestSlot, statusFlags uint32, err error) {
raw, err := d.Raw(16)
if err != nil {
return
}
copy(id[:], raw)
if sequence, err = d.Uint32(); err != nil {
return
}
if slot, err = d.Uint32(); err != nil {
return
}
if highestSlot, err = d.Uint32(); err != nil {
return
}
if targetHighestSlot, err = d.Uint32(); err != nil {
return
}
statusFlags, err = d.Uint32()
return
}
// ErrStateProtectNotSupp marks an EXCHANGE_ID that named a state
// protection this build does not speak.
var ErrStateProtectNotSupp = errors.New("nfs4: state protection not supported")
+317
View File
@@ -0,0 +1,317 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package nfs4
import (
"bytes"
"testing"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
func TestSessionIDShape(t *testing.T) {
// The session id is sixteen bytes on the wire, RFC 7863: an eight
// byte server prefix and the eight byte client id.
var prefix [8]byte
copy(prefix[:], []byte("nfs-srv-"))
id := MakeSessionID(prefix, 0xdeadbeefcafe)
if len(id) != 16 {
t.Fatalf("session id of %d bytes, want 16", len(id))
}
if id.ClientIDOf() != 0xdeadbeefcafe {
t.Fatalf("client id half: %x", id.ClientIDOf())
}
if !bytes.Equal(id[:8], prefix[:]) {
t.Fatalf("server prefix: %x", id[:8])
}
}
func TestExchangeIDRoundTrip(t *testing.T) {
var verifier [8]byte
copy(verifier[:], []byte("verif-1-"))
arg := AppendExchangeIDArgs(nil, verifier, []byte("owner-1"), ExchgIDUseNonPnfs)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpExchangeID {
t.Fatalf("op: %d, %v", op, err)
}
a, err := DecodeExchangeIDArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if a.Verifier != verifier || string(a.OwnerID) != "owner-1" ||
a.Flags != ExchgIDUseNonPnfs || a.Protect != StateProtectNone {
t.Fatalf("args: %+v", a)
}
res := AppendExchangeIDRes(nil, 77, 0, ExchgIDUseNonPnfs|ExchgIDConfirmedR, []byte("major"))
rd := xdr.NewDecoder(res)
if clientid, err := rd.Uint64(); err != nil || clientid != 77 {
t.Fatalf("clientid: %d, %v", clientid, err)
}
if seq, err := rd.Uint32(); err != nil || seq != 0 {
t.Fatalf("sequence: %d, %v", seq, err)
}
if flags, err := rd.Uint32(); err != nil || flags&ExchgIDConfirmedR == 0 {
t.Fatalf("flags: %x, %v", flags, err)
}
if n, err := resBodyLen(OpExchangeID, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
}
}
func TestCreateSessionRoundTrip(t *testing.T) {
arg := AppendCreateSessionArgs(nil, 0x1234, 3, CreateSessionFlagPersist,
DefaultForeChannel, DefaultBackChannel, 0xcafe)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpCreateSession {
t.Fatalf("op: %d, %v", op, err)
}
a, err := DecodeCreateSessionArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if a.ClientID != 0x1234 || a.Sequence != 3 || a.Flags != CreateSessionFlagPersist ||
a.Fore.MaxRequest != DefaultForeChannel.MaxRequest ||
a.Fore.MaxRequests != DefaultForeChannel.MaxRequests ||
a.Back.MaxRequest != DefaultBackChannel.MaxRequest ||
a.CBProgram != 0xcafe {
t.Fatalf("args: %+v", a)
}
id := MakeSessionID([8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 0x1234)
res := AppendCreateSessionRes(nil, id, 3, 0, DefaultForeChannel, DefaultBackChannel)
if n, err := resBodyLen(OpCreateSession, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("measure: %d of %d, %v", n, len(res), err)
}
}
func TestSequenceAndDestroyWire(t *testing.T) {
id := MakeSessionID([8]byte{9, 9, 9, 9, 9, 9, 9, 9}, 42)
arg := AppendSequenceArgs(nil, id, 5, 1, 7, true)
a, err := DecodeSequenceArgs(xdr.NewDecoder(arg[4:]))
if err != nil {
t.Fatalf("decode: %v", err)
}
if a.SessionID != id || a.Sequence != 5 || a.Slot != 1 ||
a.HighestSlot != 7 || !a.CacheThis {
t.Fatalf("args: %+v", a)
}
// The result carries exactly the five words after the session id
// that RFC 7863 fixes: sequence, slot, highest, target, flags.
res := AppendSequenceRes(nil, id, 5, 1, 7, 0)
if len(res) != 36 {
t.Fatalf("sequence result of %d bytes, want 36", len(res))
}
if n, err := resBodyLen(OpSequence, ErrOK, res); err != nil || n != 36 {
t.Fatalf("sequence measure: %d, %v", n, err)
}
flags, err := xdr.NewDecoder(res[32:]).Uint32()
if err != nil || flags != 0 {
t.Fatalf("sr_status_flags: %d, %v", flags, err)
}
sid, seq, slot, high, target, statusFlags, err := DecodeSequenceRes(xdr.NewDecoder(res))
if err != nil || sid != id || seq != 5 || slot != 1 || high != 7 ||
target != 7 || statusFlags != 0 {
t.Fatalf("decode res: %d %d %d %d %d %d, %v", seq, slot, high, target, statusFlags, 0, err)
}
destroy := AppendDestroySessionArgs(nil, id)
d := xdr.NewDecoder(destroy)
if op, err := d.Uint32(); err != nil || op != OpDestroySession {
t.Fatalf("destroy op: %d, %v", op, err)
}
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, id[:]) {
t.Fatalf("destroy id: %x, %v", raw, err)
}
}
func TestExchangeIDImplArray(t *testing.T) {
// One implementation entry walks exactly its three fields: domain,
// name and the date, the shape RFC 7863 fixes. A trailing dummy
// word stays in the stream and is the next operation's business.
var blob []byte
blob = append(blob, make([]byte, 8)...) // verifier
blob = xdr.AppendVarOpaque(blob, []byte("id")) // owner
blob = xdr.AppendUint32(blob, 0) // flags
blob = xdr.AppendUint32(blob, StateProtectNone)
blob = xdr.AppendUint32(blob, 1) // one impl entry
blob = xdr.AppendString(blob, "example.org")
blob = xdr.AppendString(blob, "probe")
blob = xdr.AppendUint64(blob, 0) // date seconds
blob = xdr.AppendUint32(blob, 0) // date nanoseconds
d := xdr.NewDecoder(blob)
a, err := DecodeExchangeIDArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if string(a.OwnerID) != "id" || d.Remaining() != 0 {
t.Fatalf("owner %q, %d bytes left", a.OwnerID, d.Remaining())
}
// The word some clients append behind the entry is not consumed by
// this decoder; the compound walk sees it as the next argop.
withExtra := append(append([]byte{}, blob...), 0, 0, 0, 0)
d = xdr.NewDecoder(withExtra)
if _, err := DecodeExchangeIDArgs(d); err != nil || d.Remaining() != 4 {
t.Fatalf("decode with extra: %v, %d bytes left", err, d.Remaining())
}
}
func TestDecodeSessionChannelIrd(t *testing.T) {
// A channel with the RDMA sizes present walks them all.
var body []byte
body = xdr.AppendUint32(body, 0) // header pad
body = xdr.AppendUint32(body, 1<<20) // max request
body = xdr.AppendUint32(body, 1<<20) // max response
body = xdr.AppendUint32(body, 1<<20) // resourced
body = xdr.AppendUint32(body, 4) // max operations
body = xdr.AppendUint32(body, 2) // max requests
body = xdr.AppendUint32(body, 2) // two rdma ird sizes
body = xdr.AppendUint32(body, 1)
body = xdr.AppendUint32(body, 4)
c, err := DecodeSessionChannel(xdr.NewDecoder(body))
if err != nil {
t.Fatalf("decode: %v", err)
}
if !c.RdmaIRDEnabled || len(c.RdmaIRSizes) != 2 || c.RdmaIRSizes[1] != 4 ||
c.MaxRequests != 2 {
t.Fatalf("channel: %+v", c)
}
}
func TestExchangeIDStateProtectReject(t *testing.T) {
// A state protection other than the plain none is refused with a
// dedicated error, because this build does not speak RPCSEC_GSS
// state protection yet.
var blob []byte
blob = append(blob, make([]byte, 8)...)
blob = xdr.AppendVarOpaque(blob, []byte("id"))
blob = xdr.AppendUint32(blob, 0)
blob = xdr.AppendUint32(blob, StateProtectMachCred)
if _, err := DecodeExchangeIDArgs(xdr.NewDecoder(blob)); err == nil {
t.Fatal("an unsupported state protection decoded")
}
}
func TestOpenCloseWire(t *testing.T) {
arg := AppendOpenArgs(nil, 0xfeed, []byte("owner"), ShareAccessBoth, uint32(0),
true, 0o640, "file.txt")
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpOpen {
t.Fatalf("op: %d, %v", op, err)
}
a, err := DecodeOpenArgs(d)
if err != nil {
t.Fatalf("decode: %v", err)
}
if !a.Create || a.Access != ShareAccessBoth || a.Deny != uint32(0) ||
a.ClientID != 0xfeed || string(a.Owner) != "owner" || a.Perm != 0o640 ||
a.Name != "file.txt" {
t.Fatalf("open args: %+v", a)
}
// NOCREATE: the create switch is absent.
arg = AppendOpenArgs(nil, 1, []byte("o"), ShareAccessRead, 0, false, 0, "there.txt")
a, err = DecodeOpenArgs(xdr.NewDecoder(arg[4:]))
if err != nil || a.Create || a.Name != "there.txt" {
t.Fatalf("nocreate args: %+v, %v", a, err)
}
res := AppendOpenRes(nil, AllZero)
if n, err := resBodyLen(OpOpen, ErrOK, res); err != nil || n != len(res) {
t.Fatalf("open measure: %d of %d, %v", n, len(res), err)
}
closeArg := AppendCloseArgs(nil, AllZero)
d = xdr.NewDecoder(closeArg)
if op, err := d.Uint32(); err != nil || op != OpClose {
t.Fatalf("close op: %d, %v", op, err)
}
if seqid, err := d.Uint32(); err != nil || seqid != 0 {
t.Fatalf("close seqid: %d, %v", seqid, err)
}
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, AllZero[:]) {
t.Fatalf("close stateid: %x, %v", raw, err)
}
closeRes := AppendCloseRes(nil, AllZero)
if n, err := resBodyLen(OpClose, ErrOK, closeRes); err != nil || n != 16 {
t.Fatalf("close measure: %d, %v", n, err)
}
}
func TestOpenDowngradeDestroyReclaimWire(t *testing.T) {
// OPEN_DOWNGRADE args: seqid, stateid, access, deny.
arg := AppendOpenDowngradeArgs(nil, AllZero, ShareAccessRead, 0)
d := xdr.NewDecoder(arg)
if op, err := d.Uint32(); err != nil || op != OpOpenDowngrade {
t.Fatalf("op: %d, %v", op, err)
}
if seqid, err := d.Uint32(); err != nil || seqid != 0 {
t.Fatalf("seqid: %d, %v", seqid, err)
}
if raw, err := d.Raw(16); err != nil || !bytes.Equal(raw, AllZero[:]) {
t.Fatalf("stateid: %x, %v", raw, err)
}
if access, err := d.Uint32(); err != nil || access != ShareAccessRead {
t.Fatalf("access: %v", err)
}
if deny, err := d.Uint32(); err != nil || deny != 0 {
t.Fatalf("deny: %v", err)
}
dgRes := AppendOpenDowngradeRes(nil)
if n, err := resBodyLen(OpOpenDowngrade, ErrOK, dgRes); err != nil || n != 20 {
t.Fatalf("downgrade measure: %d, %v", n, err)
}
// DESTROY_CLIENTID args and its empty result body.
dc := AppendDestroyClientIDArgs(nil, 0x1234)
d = xdr.NewDecoder(dc)
if op, err := d.Uint32(); err != nil || op != OpDestroyClientID {
t.Fatalf("destroy op: %d, %v", op, err)
}
if cid, err := d.Uint64(); err != nil || cid != 0x1234 {
t.Fatalf("clientid: %v", err)
}
if n, err := resBodyLen(OpDestroyClientID, ErrOK, nil); err != nil || n != 0 {
t.Fatalf("destroy measure: %d, %v", n, err)
}
// RECLAIM_COMPLETE args: the one client flag alone, RFC 7863.
rc := AppendReclaimCompleteArgs(nil, false)
d = xdr.NewDecoder(rc)
if op, err := d.Uint32(); err != nil || op != OpReclaimComplete {
t.Fatalf("op: %d, %v", op, err)
}
if one, err := d.Bool(); err != nil || one {
t.Fatalf("one_client: %v", err)
}
if d.Remaining() != 0 {
t.Fatalf("%d bytes left in the reclaim args", d.Remaining())
}
if n, err := resBodyLen(OpReclaimComplete, ErrOK, nil); err != nil || n != 0 {
t.Fatalf("reclaim measure: %d, %v", n, err)
}
}
func TestAppendOpenResDelegLength(t *testing.T) {
// The write delegation carries the space limit union and the
// permissions ace on top of the read form, RFC 7863: the whole
// successful OPEN result measures 96 bytes with one.
var st Stateid
st[0] = 1
res := AppendOpenResDeleg(nil, st, OpenDelegWrite, st)
if len(res) != 96 {
t.Fatalf("res %d bytes, want 96", len(res))
}
// The read form ends after the ace: 84 bytes.
res = AppendOpenResDeleg(nil, st, OpenDelegRead, st)
if len(res) != 84 {
t.Fatalf("read res %d bytes, want 84", len(res))
}
if _, _, _, err := DecodeOpenResDeleg(res); err != nil {
t.Fatalf("read round trip: %v", err)
}
}
@@ -0,0 +1,2 @@
go test fuzz v1
[]byte("0000\x00\x00\x00\x00\x00\x00\x000\x00\x00\x00)\x00\x00\x00\x00")
+70
View File
@@ -0,0 +1,70 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// The extended attribute operations of RFC 8276: four narrow operations
// over named opaque values.
package nfs4
import (
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// SETXATTR modes of the setxattr4mode4 enum.
const (
SetXattrUnchecked = 0
SetXattrCreate = 1
SetXattrReplace = 2
)
// AppendGetXattrArgs appends the GETXATTR argop: the name alone, the
// whole argument RFC 8276 defines.
func AppendGetXattrArgs(b []byte, name string) []byte {
b = xdr.AppendUint32(b, OpGetxattr)
return xdr.AppendString(b, name)
}
// AppendGetXattrRes appends a successful GETXATTR result: the value as
// one opaque.
func AppendGetXattrRes(b []byte, value []byte) []byte {
return xdr.AppendVarOpaque(b, value)
}
// DecodeGetXattrRes decodes a successful GETXATTR result.
func DecodeGetXattrRes(body []byte) ([]byte, error) {
return xdr.NewDecoder(body).VarOpaque()
}
// AppendSetXattrArgs appends the SETXATTR argop: the create mode, the
// name and the value.
func AppendSetXattrArgs(b []byte, mode uint32, name string, value []byte) []byte {
b = xdr.AppendUint32(b, OpSetxattr)
b = xdr.AppendUint32(b, mode)
b = xdr.AppendString(b, name)
return xdr.AppendVarOpaque(b, value)
}
// AppendListXattrArgs appends the LISTXATTR argop: the cookie the last
// reply returned and the response size budget.
func AppendListXattrArgs(b []byte, cookie uint64, maxcount uint32) []byte {
b = xdr.AppendUint32(b, OpListxattr)
b = xdr.AppendUint64(b, cookie)
return xdr.AppendUint32(b, maxcount)
}
// AppendListXattrRes appends a successful LISTXATTR result: the cookie
// of the last name, the names and the end of list flag.
func AppendListXattrRes(b []byte, cookie uint64, names []string, eof bool) []byte {
b = xdr.AppendUint64(b, cookie)
b = xdr.AppendUint32(b, uint32(len(names)))
for _, n := range names {
b = xdr.AppendString(b, n)
}
return xdr.AppendBool(b, eof)
}
// AppendRemoveXattrArgs appends the REMOVEXATTR argop: the name.
func AppendRemoveXattrArgs(b []byte, name string) []byte {
b = xdr.AppendUint32(b, OpRemovexattr)
return xdr.AppendString(b, name)
}