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
104 lines
3.3 KiB
Go
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...)
|
|
}
|