180 lines
5.4 KiB
Go
Raw Normal View History

2021-11-22 16:05:02 +00:00
package channel
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"strconv"
"strings"
)
// StrictHeader defines a framing that transmits and receives messages using a
// header prefix similar to HTTP, in which mimeType describes the content type.
//
// Specifically, each message is sent in the format:
//
// Content-Type: <mime-type>\r\n
// Content-Length: <nbytes>\r\n
// \r\n
// <payload>
//
// The length (nbytes) is encoded as decimal digits. For example, given a
// mimeType value "application/json", the message "123\n" is transmitted as:
//
// Content-Type: application/json\r\n
// Content-Length: 4\r\n
// \r\n
// 123\n
//
// If mimeType == "", the Content-Type header is omitted when sending.
//
// If the content type of an otherwise-valid received message does not match
// the expected value, Recv returns the decoded message along with an error of
// concrete type *ContentTypeMismatchError.
//
// Note: The framing returned by StrictHeader does not verify the encoding of a
// message matches the declared mimeType.
func StrictHeader(mimeType string) Framing {
return func(r io.Reader, wc io.WriteCloser) Channel {
var ctype string
if mimeType != "" {
ctype = "Content-Type: " + mimeType + "\r\n"
}
return &hdr{
mtype: mimeType,
ctype: ctype,
wc: wc,
rd: bufio.NewReader(r),
buf: bytes.NewBuffer(nil),
}
}
}
// A ContentTypeMismatchError is reported by the Recv method of a Header
// framing when the content type of the message does not match the type
// expected by the channel.
type ContentTypeMismatchError struct {
Got, Want string // the observed and expected content type values
}
func (c *ContentTypeMismatchError) Error() string {
return fmt.Sprintf("content type mismatch: got %q, want %q", c.Got, c.Want)
}
// An hdr implements Channel. Messages sent on a hdr channel are framed as a
// header/body transaction, similar to HTTP.
type hdr struct {
mtype string
ctype string
wc io.WriteCloser
rd *bufio.Reader
buf *bytes.Buffer
rbuf []byte
}
// Send implements part of the Channel interface.
func (h *hdr) Send(msg []byte) error {
h.buf.Reset()
if h.ctype != "" {
h.buf.WriteString(h.ctype)
}
h.buf.WriteString("Content-Length: ")
h.buf.WriteString(strconv.Itoa(len(msg)))
h.buf.WriteString("\r\n\r\n")
h.buf.Write(msg)
_, err := h.wc.Write(h.buf.Next(h.buf.Len()))
return err
}
// Recv implements part of the Channel interface. If the content type of the
// received message does not match the expected value, Recv returns the decoded
// message along with an error of concrete type *ContentTypeMismatchError. The
// caller may choose to ignore this error by testing explicitly for this type.
func (h *hdr) Recv() ([]byte, error) {
var contentType, contentLength string
for {
raw, err := h.rd.ReadString('\n')
if err == io.EOF && raw != "" {
// handle a partial line at EOF
} else if err != nil {
return nil, err
}
if line := strings.TrimRight(raw, "\r\n"); line == "" {
break
} else if parts := strings.SplitN(line, ":", 2); len(parts) == 2 {
// This implementation ignores unknown header fields.
clean := strings.TrimSpace(parts[1])
switch strings.ToLower(parts[0]) {
case "content-type":
contentType = clean
case "content-length":
contentLength = clean
}
} else {
return nil, errors.New("invalid header line")
}
}
// Verify that the content-type matches what we expect, but defer reporting
// it until the message has been fully decoded.
var contentErr error
if contentType != h.mtype {
contentErr = &ContentTypeMismatchError{Got: contentType, Want: h.mtype}
}
// Parse out the required content-length field.
if contentLength == "" {
return nil, errors.New("missing required content-length")
}
size, err := strconv.Atoi(contentLength)
if err != nil || size < 0 {
return nil, errors.New("invalid content-length")
}
// We need to use ReadFull here because the buffered reader may not have a
// big enough buffer to deliver the whole message, and will only issue a
// single read to the underlying source.
data := h.rbuf
if len(data) < size || len(data) > (1<<20) && size < len(data)/4 {
data = make([]byte, size*2)
h.rbuf = data
}
if _, err := io.ReadFull(h.rd, data[:size]); err != nil {
return nil, err
}
return data[:size], contentErr
}
// Close implements part of the Channel interface.
func (h *hdr) Close() error { return h.wc.Close() }
// Header returns a framing that behaves as StrictHeader, but allows received
// messages to omit the Content-Type header without error. An error will still
// be reported if a content-type is set but does not match.
func Header(mimeType string) Framing {
strict := StrictHeader(mimeType)
return func(r io.Reader, wc io.WriteCloser) Channel {
return opthdr{strict(r, wc).(*hdr)}
}
}
// An opthdr is a wrapper around hdr that filters out the error reported when
// the inbound message does not specify a content-type.
type opthdr struct{ *hdr }
func (o opthdr) Recv() ([]byte, error) {
msg, err := o.hdr.Recv()
if v, ok := err.(*ContentTypeMismatchError); ok && v.Got == "" {
err = nil
}
return msg, err
}
// LSP is a header framing (see Header) that transmits and receives messages on
// r and wc using the MIME type application/vscode-jsonrpc. This is the format
// preferred by the Language Server Protocol (LSP), defined by
// https://microsoft.github.io/language-server-protocol
var LSP = Header("application/vscode-jsonrpc; charset=utf-8")