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
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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
@@ -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[:]...)
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user