Vectrum
Sign In
133 lines · 3.5 KB · Go
package diag

import (
	"bufio"
	"bytes"
	"io"
	"strings"
	"testing"
	"time"
)

// trickle hands out at most n bytes per Read, so a message arrives in as many
// pieces as a pipe might deliver it in.
type trickle struct {
	r io.Reader
	n int
}

func (t trickle) Read(p []byte) (int, error) {
	if len(p) > t.n {
		p = p[:t.n]
	}
	return t.r.Read(p)
}

func TestReadMessageBodyArrivingInPieces(t *testing.T) {
	body := `{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}`
	var buf bytes.Buffer
	if err := writeMessage(&buf, []byte(body)); err != nil {
		t.Fatal(err)
	}
	// One byte at a time is the worst case a pipe can do; bufio's own
	// buffering must not be what makes this pass, so its size is tiny too.
	r := bufio.NewReaderSize(trickle{&buf, 1}, 16)
	got, err := readMessage(r)
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != body {
		t.Errorf("got %q, want %q", got, body)
	}
}

func TestReadMessageHeaderSplitAcrossWrites(t *testing.T) {
	pr, pw := io.Pipe()
	go func() {
		defer pw.Close()
		for _, piece := range []string{"Content-Le", "ngth: 2", "\r\n", "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n", "\r", "\n", "{", "}"} {
			_, _ = io.WriteString(pw, piece)
			time.Sleep(2 * time.Millisecond)
		}
	}()
	got, err := readMessage(bufio.NewReader(pr))
	if err != nil {
		t.Fatal(err)
	}
	if string(got) != "{}" {
		t.Errorf("got %q", got)
	}
}

func TestReadMessageTwoMessagesBackToBack(t *testing.T) {
	var buf bytes.Buffer
	_ = writeMessage(&buf, []byte(`{"a":1}`))
	_ = writeMessage(&buf, []byte(`{"b":2}`))
	r := bufio.NewReader(&buf)
	for _, want := range []string{`{"a":1}`, `{"b":2}`} {
		got, err := readMessage(r)
		if err != nil {
			t.Fatal(err)
		}
		if string(got) != want {
			t.Errorf("got %q, want %q", got, want)
		}
	}
	if _, err := readMessage(r); err != io.EOF {
		t.Errorf("after the last message: %v, want io.EOF", err)
	}
}

func TestReadMessageRejectsBadFraming(t *testing.T) {
	cases := map[string]string{
		"no content-length": "Content-Type: x\r\n\r\n{}",
		"garbage header":    "hello\r\n\r\n",
		"negative length":   "Content-Length: -1\r\n\r\n",
		"non-numeric":       "Content-Length: abc\r\n\r\n",
		"over the limit":    "Content-Length: 99999999999\r\n\r\n",
		"truncated body":    "Content-Length: 10\r\n\r\n{}",
		"truncated header":  "Content-Length: 2",
	}
	for name, in := range cases {
		t.Run(name, func(t *testing.T) {
			done := make(chan error, 1)
			go func() {
				_, err := readMessage(bufio.NewReader(strings.NewReader(in)))
				done <- err
			}()
			select {
			case err := <-done:
				if err == nil {
					t.Errorf("%q was accepted", in)
				}
				if err == io.EOF {
					t.Errorf("%q reported clean EOF rather than an error", in)
				}
			case <-time.After(time.Second):
				t.Fatal("readMessage hung")
			}
		})
	}
}

func TestReadMessageOversizeIsNotAllocated(t *testing.T) {
	// The limit check has to happen before the make, or a hostile header
	// would be a multi-gigabyte allocation. The over-the-limit case above
	// proves the error; this one proves the body is not read either, by
	// handing over a reader that would block forever if asked.
	pr, _ := io.Pipe()
	header := strings.NewReader("Content-Length: 99999999\r\n\r\n")
	done := make(chan error, 1)
	go func() {
		_, err := readMessage(bufio.NewReader(io.MultiReader(header, pr)))
		done <- err
	}()
	select {
	case err := <-done:
		if err == nil {
			t.Error("oversize message accepted")
		}
	case <-time.After(time.Second):
		t.Fatal("readMessage tried to read an oversize body")
	}
}