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