Files
nfs/internal/rpc/record.go
petrbalvin a9b8039ef7
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
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

104 lines
3.3 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
// Package rpc implements the record marking layer of ONC RPC, RFC 5531.
//
// An ONC RPC message travels over a byte stream as one record: a sequence of
// one or more fragments, each headed by a 32 bit word whose high bit marks
// the last fragment of the record and whose low 31 bits carry the fragment
// length in bytes.
package rpc
import (
"errors"
"fmt"
"io"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// LastFragment is the high bit of a fragment header, set on the final
// fragment of a record.
const LastFragment = 1 << 31
// maxFragment is the largest fragment length the low 31 bits can carry.
const maxFragment = 1<<31 - 1
// ErrRecordTooLarge is returned by ReadRecord when a record exceeds the
// caller's limit.
var ErrRecordTooLarge = errors.New("rpc: record exceeds the size limit")
// AppendFragmentHeader appends the record marking header of a fragment
// that carries n bytes. It panics when n is negative or above the
// largest fragment length; callers reach it through WriteRecord, which
// rejects such input with ErrRecordTooLarge instead.
func AppendFragmentHeader(b []byte, n int, last bool) []byte {
if n < 0 || n > maxFragment {
panic(fmt.Sprintf("rpc: fragment length %d out of range", n))
}
h := uint32(n)
if last {
h |= LastFragment
}
return xdr.AppendUint32(b, h)
}
// WriteRecord writes data to w as one record in a single final fragment.
// The caller keeps the record under maxFragment bytes; a call that carries a
// whole ONC RPC request or reply always fits.
func WriteRecord(w io.Writer, data []byte) error {
if len(data) > maxFragment {
return ErrRecordTooLarge
}
buf := AppendFragmentHeader(make([]byte, 0, 4+len(data)), len(data), true)
buf = append(buf, data...)
_, err := w.Write(buf)
return err
}
// ReadRecord reads one record from r and returns its reassembled bytes. The
// record may arrive in any number of fragments and may exceed the reader's
// own buffer only up to limit bytes; a longer record returns
// ErrRecordTooLarge before the limit is exceeded in memory.
func ReadRecord(r io.Reader, limit int) ([]byte, error) {
var header [4]byte
var record []byte
for {
if _, err := io.ReadFull(r, header[:]); err != nil {
if errors.Is(err, io.EOF) && len(record) == 0 {
return nil, io.EOF
}
return nil, fmt.Errorf("rpc: fragment header: %w", err)
}
h, err := xdr.NewDecoder(header[:]).Uint32()
if err != nil {
// Unreachable: a four byte input always holds a uint32.
return nil, err
}
n := int(h &^ LastFragment)
if n > limit-len(record) {
return nil, ErrRecordTooLarge
}
start := len(record)
record = append(record, make([]byte, n)...)
if _, err := io.ReadFull(r, record[start:]); err != nil {
return nil, fmt.Errorf("rpc: fragment body: %w", err)
}
if h&LastFragment != 0 {
return record, nil
}
}
}
// AppendRecord appends the record marking of one last fragment and the
// data behind it, for a caller that owns the destination buffer: a
// writer that recycles its wire buffers through this function saves the
// allocation WriteRecord makes per call.
func AppendRecord(dst, data []byte) []byte {
if len(data) > maxFragment {
return dst
}
dst = AppendFragmentHeader(dst, len(data), true)
return append(dst, data...)
}