Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions internal/sandboxwire/codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package sandboxwire

import (
"encoding/binary"
"fmt"
)

// Encoder appends primitives to a payload. The zero value is ready to use.
type Encoder struct {
b []byte
}

// Payload returns the encoded bytes.
func (e *Encoder) Payload() []byte { return e.b }

func (e *Encoder) U8(v uint8) { e.b = append(e.b, v) }
func (e *Encoder) U16(v uint16) { e.b = binary.BigEndian.AppendUint16(e.b, v) }
func (e *Encoder) U32(v uint32) { e.b = binary.BigEndian.AppendUint32(e.b, v) }
func (e *Encoder) U64(v uint64) { e.b = binary.BigEndian.AppendUint64(e.b, v) }
func (e *Encoder) I64(v int64) { e.U64(uint64(v)) }

// Bool writes 1 for true and 0 for false.
func (e *Encoder) Bool(v bool) {
if v {
e.U8(1)
} else {
e.U8(0)
}
}

// Bytes writes a uint32 length followed by v.
func (e *Encoder) Bytes(v []byte) {
e.U32(uint32(len(v)))
e.b = append(e.b, v...)
}

// Count writes the uint32 element count of an array.
func (e *Encoder) Count(n int) { e.U32(uint32(n)) }

// Present writes the presence byte of an optional field; the value follows only
// when ok.
func (e *Encoder) Present(ok bool) { e.Bool(ok) }

// Enum writes a uint16 enum value. Zero is invalid on the wire.
func (e *Encoder) Enum(v uint16) { e.U16(v) }

func (e *Encoder) ID(id ID) { e.b = append(e.b, id[:]...) }

func (e *Encoder) Effect(v Effect) { e.Enum(uint16(v)) }

// Decoder reads primitives from a payload. Every error wraps ErrMalformed;
// after an error the Decoder's position is unspecified.
type Decoder struct {
b []byte
}

func NewDecoder(payload []byte) *Decoder { return &Decoder{b: payload} }

// Remaining returns the number of unread bytes.
func (d *Decoder) Remaining() int { return len(d.b) }

// Finish fails when bytes remain unread.
func (d *Decoder) Finish() error {
if len(d.b) != 0 {
return fmt.Errorf("%w: %d trailing bytes", ErrMalformed, len(d.b))
}
return nil
}

func (d *Decoder) take(n uint64, what string) ([]byte, error) {
if n > uint64(len(d.b)) {
return nil, fmt.Errorf("%w: %s needs %d bytes, %d remain", ErrMalformed, what, n, len(d.b))
}
v := d.b[:n:n]
d.b = d.b[n:]
return v, nil
}

func (d *Decoder) U8() (uint8, error) {
b, err := d.take(1, "u8")
if err != nil {
return 0, err
}
return b[0], nil
}

func (d *Decoder) U16() (uint16, error) {
b, err := d.take(2, "u16")
if err != nil {
return 0, err
}
return binary.BigEndian.Uint16(b), nil
}

func (d *Decoder) U32() (uint32, error) {
b, err := d.take(4, "u32")
if err != nil {
return 0, err
}
return binary.BigEndian.Uint32(b), nil
}

func (d *Decoder) U64() (uint64, error) {
b, err := d.take(8, "u64")
if err != nil {
return 0, err
}
return binary.BigEndian.Uint64(b), nil
}

func (d *Decoder) I64() (int64, error) {
v, err := d.U64()
return int64(v), err
}

// Bool accepts exactly 0 or 1.
func (d *Decoder) Bool() (bool, error) {
v, err := d.U8()
if err == nil && v > 1 {
err = fmt.Errorf("%w: boolean byte %d", ErrMalformed, v)
}
return v == 1, err
}

// Bytes reads a uint32 length and that many bytes. The result aliases the
// payload, with its capacity clipped to its length.
func (d *Decoder) Bytes() ([]byte, error) {
n, err := d.U32()
if err != nil {
return nil, err
}
return d.take(uint64(n), "byte string")
}

// Count reads an array's uint32 element count. The count is at most max and at
// most the remaining bytes, because every element encodes to at least one byte,
// so a caller may allocate count elements.
func (d *Decoder) Count(max uint32) (int, error) {
n, err := d.U32()
switch {
case err != nil:
return 0, err
case n > max:
return 0, fmt.Errorf("%w: count %d exceeds maximum %d", ErrMalformed, n, max)
case uint64(n) > uint64(len(d.b)):
return 0, fmt.Errorf("%w: count %d exceeds %d remaining bytes", ErrMalformed, n, len(d.b))
}
return int(n), nil
}

// Present reads the presence byte of an optional field.
func (d *Decoder) Present() (bool, error) { return d.Bool() }

// Enum reads a uint16 enum value, rejecting zero and values valid does not
// accept.
func (d *Decoder) Enum(valid func(uint16) bool) (uint16, error) {
v, err := d.U16()
if err == nil && (v == 0 || !valid(v)) {
err = fmt.Errorf("%w: enum value %d", ErrMalformed, v)
}
return v, err
}

// ID reads a required identifier, rejecting the zero ID.
func (d *Decoder) ID() (ID, error) {
b, err := d.take(uint64(len(ID{})), "identifier")
if err != nil {
return ID{}, err
}
if id := ID(b); !id.IsZero() {
return id, nil
}
return ID{}, fmt.Errorf("%w: zero identifier", ErrMalformed)
}

func (d *Decoder) Effect() (Effect, error) {
v, err := d.Enum(func(v uint16) bool { return Effect(v).Valid() })
return Effect(v), err
}
134 changes: 134 additions & 0 deletions internal/sandboxwire/frame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Package sandboxwire holds the frame header and the primitive encoding shared
// by the File, Process and Link protocols, plus the few values more than one of
// them uses.
//
// It defines no messages. Each protocol's protocol.go owns its message tags,
// ordered payload layouts, validators and encoding. On the wire an enum is a
// uint16 whose zero value is invalid, and an optional field is a presence byte
// followed by the value.
package sandboxwire

import (
"encoding/binary"
"errors"
"fmt"
"io"
)

const (
// HeaderSize is the length of the frame header: PayloadLength uint32,
// MessageType uint16, Flags uint16 and RequestID uint64, in network byte
// order.
HeaderSize = 16
// MaxPayload is the hard maximum frame payload. Advertised limits may be
// smaller.
MaxPayload = 1 << 20
// MaxChunk is the maximum file or process data chunk.
MaxChunk = 64 << 10
)

// ErrMalformed is the single error for bytes that violate the wire rules.
// Errors from this package and from protocol decoders wrap it.
var ErrMalformed = errors.New("sandboxwire: malformed message")

// Frame is one decoded frame. RequestID is zero only for unsolicited events.
type Frame struct {
Type uint16
RequestID uint64
Payload []byte
}

// ReadFrame reads one frame. It rejects a payload length above maxPayload
// (capped at MaxPayload) before allocating, and rejects nonzero flags. It
// returns io.EOF only when r ends before the first header byte.
func ReadFrame(r io.Reader, maxPayload uint32) (Frame, error) {
var h [HeaderSize]byte
if _, err := io.ReadFull(r, h[:]); err != nil {
return Frame{}, err
}
n, limit := binary.BigEndian.Uint32(h[0:4]), min(maxPayload, MaxPayload)
if n > limit {
return Frame{}, fmt.Errorf("%w: payload length %d exceeds limit %d", ErrMalformed, n, limit)
}
if flags := binary.BigEndian.Uint16(h[6:8]); flags != 0 {
return Frame{}, fmt.Errorf("%w: frame flags %#04x", ErrMalformed, flags)
}
f := Frame{
Type: binary.BigEndian.Uint16(h[4:6]),
RequestID: binary.BigEndian.Uint64(h[8:16]),
Payload: make([]byte, n),
}
if _, err := io.ReadFull(r, f.Payload); err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return Frame{}, err
}
return f, nil
}

// WriteFrame writes f with a single Write call, so frames written by goroutines
// sharing w under a mutex never interleave.
func WriteFrame(w io.Writer, f Frame) error {
if len(f.Payload) > MaxPayload {
return fmt.Errorf("%w: payload length %d exceeds limit %d", ErrMalformed, len(f.Payload), MaxPayload)
}
b := make([]byte, HeaderSize+len(f.Payload))
binary.BigEndian.PutUint32(b[0:4], uint32(len(f.Payload)))
binary.BigEndian.PutUint16(b[4:6], f.Type)
binary.BigEndian.PutUint64(b[8:16], f.RequestID)
copy(b[HeaderSize:], f.Payload)
_, err := w.Write(b)
return err
}

const responseBit = 0x8000

// FirstEvent is the first event tag. A protocol numbers its events upward from
// it in the order it lists them.
const FirstEvent = 0x4001

// ResponseType returns the tag of the response to request tag req.
func ResponseType(req uint16) uint16 { return req | responseBit }

// IsResponse reports whether t is a response tag.
func IsResponse(t uint16) bool { return t&responseBit != 0 }

// ValidRequestID reports whether id may identify a request. Zero is reserved
// for unsolicited events.
func ValidRequestID(id uint64) bool { return id != 0 }

// Kind is the role of a message type.
type Kind uint8

const (
KindRequest Kind = iota + 1
KindResponse
KindEvent
)

// Tags is a protocol's known message types: requests 1 through Requests, their
// responses, and events FirstEvent through FirstEvent+Events-1. Requests stays
// below FirstEvent.
type Tags struct {
Requests uint16
Events uint16
}

// Classify returns the kind of message type t, or ErrMalformed when t is not
// one of the known tags.
func (k Tags) Classify(t uint16) (Kind, error) {
switch {
case IsResponse(t):
if req := t &^ responseBit; req >= 1 && req <= k.Requests {
return KindResponse, nil
}
case t >= FirstEvent:
if t-FirstEvent < k.Events {
return KindEvent, nil
}
case t >= 1 && t <= k.Requests:
return KindRequest, nil
}
return 0, fmt.Errorf("%w: unknown message type %#04x", ErrMalformed, t)
}
Loading
Loading