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