// voice/frame.go — length-prefixed JSON framing. // // Same shape as ipc/worker with the 64 MiB cap (see wire.go maxFrame). One // frame carries EITHER a Request, Response, or Push — distinguished by the // present fields (Request has `id`+`m`; Response has `id`; Push has `kind`). // Server-side reads probe all three shallowly; client-side reads expect // either Response or Push depending on context. package voice import ( "encoding/binary" "encoding/json" "errors" "fmt" "io" ) // ErrFrameTooLarge — frame exceeded maxFrame; conn is desynced; caller must close. var ErrFrameTooLarge = errors.New("voice: frame too large") func writeFrame(w io.Writer, v any) error { body, err := json.Marshal(v) if err != nil { return fmt.Errorf("voice: marshal frame: %w", err) } if len(body) > maxFrame { return fmt.Errorf("%w: %d bytes", ErrFrameTooLarge, len(body)) } var hdr [4]byte binary.BigEndian.PutUint32(hdr[:], uint32(len(body))) if _, err := w.Write(hdr[:]); err != nil { return fmt.Errorf("voice: write frame header: %w", err) } if _, err := w.Write(body); err != nil { return fmt.Errorf("voice: write frame body: %w", err) } return nil } func readFrame(r io.Reader, v any) error { var hdr [4]byte if _, err := io.ReadFull(r, hdr[:]); err != nil { if errors.Is(err, io.EOF) { return io.EOF } return fmt.Errorf("voice: read frame header: %w", err) } n := binary.BigEndian.Uint32(hdr[:]) if n > maxFrame { return fmt.Errorf("%w: %d bytes", ErrFrameTooLarge, n) } buf := make([]byte, n) if _, err := io.ReadFull(r, buf); err != nil { return fmt.Errorf("voice: read frame body: %w", err) } if err := json.Unmarshal(buf, v); err != nil { return fmt.Errorf("voice: unmarshal frame: %w", err) } return nil }