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: \r\n // Content-Length: \r\n // \r\n // // // 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")