feat: publish the Hime-san Beta 2 LSP
Add the standard-library language server, editor-neutral protocol contract, additive version feature discovery, and bounded security regressions while leaving the Sando runtime unchanged. Material design and implementation assistance was provided by OpenAI Codex. Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package lsp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type shortWriter struct{}
|
||||
|
||||
func (shortWriter) Write(value []byte) (int, error) {
|
||||
if len(value) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return len(value) - 1, nil
|
||||
}
|
||||
|
||||
func TestProtocolFramingRoundTripAndLimits(t *testing.T) {
|
||||
t.Parallel()
|
||||
var output bytes.Buffer
|
||||
writer := &frameWriter{output: &output}
|
||||
if err := writer.write(rpcMessage{JSONRPC: "2.0", ID: json.RawMessage("1"), Result: map[string]bool{"ok": true}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := newFrameReader(&output).read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded rpcMessage
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(decoded.ID) != "1" {
|
||||
t.Fatalf("response ID = %s", decoded.ID)
|
||||
}
|
||||
|
||||
invalid := "Content-Length: 999999999\r\n\r\n"
|
||||
if _, err := newFrameReader(strings.NewReader(invalid)).read(); err == nil {
|
||||
t.Fatal("excessive frame length was accepted")
|
||||
}
|
||||
duplicate := "Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}"
|
||||
if _, err := newFrameReader(strings.NewReader(duplicate)).read(); err == nil {
|
||||
t.Fatal("duplicate Content-Length was accepted")
|
||||
}
|
||||
var excessive bytes.Buffer
|
||||
for range maxHeaderLines + 1 {
|
||||
excessive.WriteString("X-Test: value\r\n")
|
||||
}
|
||||
excessive.WriteString("Content-Length: 2\r\n\r\n{}")
|
||||
if _, err := newFrameReader(&excessive).read(); err == nil {
|
||||
t.Fatal("excessive header count was accepted")
|
||||
}
|
||||
if err := (&frameWriter{output: shortWriter{}}).write(rpcMessage{JSONRPC: "2.0", ID: json.RawMessage("1"), Result: true}); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Fatalf("short writer error = %v, want io.ErrShortWrite", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedJSONProducesProtocolErrorAndContinues(t *testing.T) {
|
||||
t.Parallel()
|
||||
input := bytes.NewBuffer(nil)
|
||||
input.WriteString("Content-Length: 1\r\n\r\n{")
|
||||
exit, err := json.Marshal(rpcMessage{JSONRPC: "2.0", Method: "exit"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fmt.Fprintf(input, "Content-Length: %d\r\n\r\n", len(exit))
|
||||
input.Write(exit)
|
||||
var output bytes.Buffer
|
||||
if err := Run(context.Background(), Options{Input: input, Output: &output}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := newFrameReader(&output).read()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var response rpcMessage
|
||||
if err := json.Unmarshal(payload, &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.Error == nil || response.Error.Code != errParse {
|
||||
t.Fatalf("parse error response = %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestIDValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, id := range []string{`1`, `-2`, `"request"`} {
|
||||
if !validRequestID(json.RawMessage(id)) {
|
||||
t.Errorf("valid request ID rejected: %s", id)
|
||||
}
|
||||
}
|
||||
for _, id := range []string{`null`, `true`, `1.5`, `{}`, `[]`, `not-json`} {
|
||||
if validRequestID(json.RawMessage(id)) {
|
||||
t.Errorf("invalid request ID accepted: %s", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUTF16PositionsWithUnicodeAndCRLF(t *testing.T) {
|
||||
t.Parallel()
|
||||
text := []byte("a😀b\r\n雪c\n")
|
||||
tests := []struct {
|
||||
offset int
|
||||
position Position
|
||||
}{
|
||||
{offset: 0, position: Position{Line: 0, Character: 0}},
|
||||
{offset: 1, position: Position{Line: 0, Character: 1}},
|
||||
{offset: 5, position: Position{Line: 0, Character: 3}},
|
||||
{offset: 8, position: Position{Line: 1, Character: 0}},
|
||||
{offset: 11, position: Position{Line: 1, Character: 1}},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := offsetToPosition(text, test.offset); got != test.position {
|
||||
t.Errorf("offsetToPosition(%d) = %#v, want %#v", test.offset, got, test.position)
|
||||
}
|
||||
if got, ok := positionToOffset(text, test.position); !ok || got != test.offset {
|
||||
t.Errorf("positionToOffset(%#v) = %d, %v; want %d, true", test.position, got, ok, test.offset)
|
||||
}
|
||||
}
|
||||
if _, ok := positionToOffset(text, Position{Line: 0, Character: 2}); ok {
|
||||
t.Fatal("position inside UTF-16 surrogate pair was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzFrameReaderNeverPanics(f *testing.F) {
|
||||
f.Add([]byte("Content-Length: 2\r\n\r\n{}"))
|
||||
f.Add([]byte("Content-Length: nope\r\n\r\n"))
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
if len(data) > 64<<10 {
|
||||
t.Skip()
|
||||
}
|
||||
_, _ = newFrameReader(bytes.NewReader(data)).read()
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzDocumentPositionNeverPanics(f *testing.F) {
|
||||
f.Add("hello 😀\r\nworld", 0, 7)
|
||||
f.Add("雪", 0, 1)
|
||||
f.Fuzz(func(t *testing.T, text string, line, character int) {
|
||||
if len(text) > 64<<10 || line < -10000 || line > 10000 || character < -10000 || character > 100000 {
|
||||
t.Skip()
|
||||
}
|
||||
offset, ok := positionToOffset([]byte(text), Position{Line: line, Character: character})
|
||||
if ok {
|
||||
_ = offsetToPosition([]byte(text), offset)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user