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:
2026-08-12 20:31:07 -04:00
parent e3a94fd16b
commit 1082d9d61e
25 changed files with 2463 additions and 89 deletions
+155
View File
@@ -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)
}
})
}