turbo-editors/turbo-corepublic Fork 0
v1.0.2
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

🛟 Updated. 28d5985 · on v1.0.2 · k33g · 16h ago
framing.go · 102 lines · 3.5 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
// Package lsp is a small Language Server Protocol client: enough of it to ask
// a language server for completions, hovers, definitions and diagnostics.
//
// The protocol is JSON-RPC 2.0 inside HTTP-style frames. The JSON-RPC half is
// jsonrpc's, shared with every other protocol built on it; what is here is the
// framing, the methods, and the conversation a language server expects.
package lsp

import (
	"bufio"
	"errors"
	"fmt"
	"io"
	"net/textproto"
	"strconv"
)

// MaxMessageBytes caps how large a single message may be. A language server
// should never send anything near this; the limit is there so that a corrupt
// or hostile Content-Length cannot make the editor allocate without bound.
const MaxMessageBytes = 64 << 20

// ErrMessageTooLarge is returned when a frame announces more bytes than
// MaxMessageBytes.
var ErrMessageTooLarge = errors.New("lsp: message too large")

// contentLengthHeader is the one header the protocol actually requires.
const contentLengthHeader = "Content-Length"

// Framing is the Language Server Protocol's way of marking one message off
// from the next: a Content-Length header, a blank line, then the JSON.
//
// It is the jsonrpc.Framer a language server connection is built with.
//
//	conn := jsonrpc.NewConn(stream, lsp.Framing{}, onNotify, onRequest)
type Framing struct{}

// ReadMessage reads one framed message and returns its body.
func (Framing) ReadMessage(r *bufio.Reader) ([]byte, error) { return ReadMessage(r) }

// WriteMessage frames body and writes it out.
func (Framing) WriteMessage(w io.Writer, body []byte) error { return WriteMessage(w, body) }

// WriteMessage frames body and writes it out.
//
// The frame is the header, a blank line, then the JSON — the same shape HTTP
// uses, which is what the specification asks for.
func WriteMessage(w io.Writer, body []byte) error {
	if _, err := fmt.Fprintf(w, "%s: %d\r\n\r\n", contentLengthHeader, len(body)); err != nil {
		return fmt.Errorf("lsp: writing the header: %w", err)
	}
	if _, err := w.Write(body); err != nil {
		return fmt.Errorf("lsp: writing the body: %w", err)
	}
	return nil
}

// ReadMessage reads one framed message and returns its body.
//
// It returns io.EOF when the stream ends cleanly between messages, which is
// how a server shutting down is told apart from one that died mid-message.
func ReadMessage(r *bufio.Reader) ([]byte, error) {
	length, err := readContentLength(r)
	if err != nil {
		return nil, err
	}

	body := make([]byte, length)
	if _, err := io.ReadFull(r, body); err != nil {
		return nil, fmt.Errorf("lsp: reading a %d-byte body: %w", length, err)
	}
	return body, nil
}

// readContentLength reads the header block and returns the announced size.
func readContentLength(r *bufio.Reader) (int, error) {
	header, err := textproto.NewReader(r).ReadMIMEHeader()
	if err != nil {
		if errors.Is(err, io.EOF) {
			return 0, io.EOF
		}
		return 0, fmt.Errorf("lsp: reading the header: %w", err)
	}
	return parseContentLength(header.Get(contentLengthHeader))
}

// parseContentLength turns the header's value into a size, rejecting the ones
// that are missing, malformed, or large enough to be an attack.
func parseContentLength(raw string) (int, error) {
	if raw == "" {
		return 0, fmt.Errorf("lsp: the frame has no %s header", contentLengthHeader)
	}

	length, err := strconv.Atoi(raw)
	if err != nil || length < 0 {
		return 0, fmt.Errorf("lsp: %s is %q, which is not a size", contentLengthHeader, raw)
	}
	if length > MaxMessageBytes {
		return 0, fmt.Errorf("%w: %d bytes", ErrMessageTooLarge, length)
	}
	return length, nil
}