package diag
import (
"bufio"
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
// maxMessageBytes is the largest JSON-RPC message the client will read. A
// language server that announces a body larger than this is either confused
// or hostile, and either way the right answer is to drop the connection
// rather than to allocate what it asked for. Sixteen megabytes is far past
// any diagnostics batch and comfortably under the point where a reader
// goroutine's allocation shows up in the harness's memory.
const maxMessageBytes = 16 << 20
// readMessage reads one JSON-RPC message in the LSP's base-protocol framing:
// header lines terminated by CRLF, a blank line, then exactly Content-Length
// bytes of body.
//
// The body is read with io.ReadFull rather than a single Read, because a pipe
// hands over whatever the server has written so far and a 40-kilobyte
// diagnostics batch arrives in several pieces. The header is read line by
// line off a bufio.Reader for the same reason: "Content-Le" followed later by
// "ngth: 5" is one header, and a reader that gave up between them would take
// the rest of the stream for garbage.
func readMessage(r *bufio.Reader) ([]byte, error) {
length := -1
for {
line, err := r.ReadString('\n')
if err != nil {
if err == io.EOF && line == "" && length < 0 {
return nil, io.EOF
}
return nil, fmt.Errorf("reading header: %w", err)
}
line = strings.TrimRight(line, "\r\n")
if line == "" {
break
}
name, value, ok := strings.Cut(line, ":")
if !ok {
return nil, fmt.Errorf("malformed header line %q", line)
}
if strings.EqualFold(strings.TrimSpace(name), "Content-Length") {
n, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || n < 0 {
return nil, fmt.Errorf("bad Content-Length %q", strings.TrimSpace(value))
}
length = n
}
// Content-Type is the only other header the spec defines, and its
// only permitted charset is utf-8, so there is nothing to do with it.
}
if length < 0 {
return nil, fmt.Errorf("message has no Content-Length header")
}
if length > maxMessageBytes {
return nil, fmt.Errorf("message of %d bytes exceeds the %d-byte limit", length, maxMessageBytes)
}
body := make([]byte, length)
if _, err := io.ReadFull(r, body); err != nil {
return nil, fmt.Errorf("reading %d-byte body: %w", length, err)
}
return body, nil
}
// writeMessage frames body and writes it in one call, so two goroutines
// sharing the writer under a mutex cannot interleave a header with a body.
func writeMessage(w io.Writer, body []byte) error {
var buf bytes.Buffer
buf.Grow(len(body) + 32)
fmt.Fprintf(&buf, "Content-Length: %d\r\n\r\n", len(body))
buf.Write(body)
_, err := w.Write(buf.Bytes())
return err
}