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