summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/framing.test.ts
blob: 721665c1ce3d848c72798d0fc55ad9d90ba6e1d4 (plain)
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import { describe, expect, it } from "vitest";
import { encode, FrameDecoder } from "./framing.js";

describe("framing", () => {
	it("encode/decode round-trips", () => {
		const msg = JSON.stringify({ jsonrpc: "2.0", method: "test", params: { a: 1 } });
		const encoded = encode(msg);
		const decoder = new FrameDecoder();
		const messages = decoder.decode(encoded);
		expect(messages).toHaveLength(1);
		expect(messages[0]).toBe(msg);
	});

	it("decoder reassembles a frame split across two chunks", () => {
		const msg = JSON.stringify({ jsonrpc: "2.0", method: "test" });
		const encoded = encode(msg);
		const mid = Math.floor(encoded.length / 2);
		const chunk1 = encoded.slice(0, mid);
		const chunk2 = encoded.slice(mid);

		const decoder = new FrameDecoder();
		const result1 = decoder.decode(chunk1);
		expect(result1).toHaveLength(0);

		const result2 = decoder.decode(chunk2);
		expect(result2).toHaveLength(1);
		expect(result2[0]).toBe(msg);
	});

	it("decoder yields two messages from one chunk", () => {
		const msg1 = JSON.stringify({ jsonrpc: "2.0", method: "a" });
		const msg2 = JSON.stringify({ jsonrpc: "2.0", method: "b" });
		const encoded1 = encode(msg1);
		const encoded2 = encode(msg2);

		const combined = new Uint8Array(encoded1.length + encoded2.length);
		combined.set(encoded1);
		combined.set(encoded2, encoded1.length);

		const decoder = new FrameDecoder();
		const messages = decoder.decode(combined);
		expect(messages).toHaveLength(2);
		expect(messages[0]).toBe(msg1);
		expect(messages[1]).toBe(msg2);
	});
});

describe("multi-byte UTF-8", () => {
	it("round-trips a message with multi-byte characters", () => {
		const msg = JSON.stringify({
			jsonrpc: "2.0",
			method: "textDocument/publishDiagnostics",
			params: { message: "Type '漢字' is not assignable to type 'number'. 🚫" },
		});
		const encoded = encode(msg);
		const decoder = new FrameDecoder();
		const messages = decoder.decode(encoded);
		expect(messages).toHaveLength(1);
		expect(messages[0]).toBe(msg);
	});

	it("reassembles a multi-byte message split at a character boundary", () => {
		// A message whose JSON body contains 3-byte UTF-8 characters (漢字).
		// We split the encoded frame so the boundary falls INSIDE a multi-byte
		// sequence — the old string-based decoder would corrupt this.
		const msg = JSON.stringify({
			jsonrpc: "2.0",
			method: "test",
			params: { text: "漢字テスト" },
		});
		const encoded = encode(msg);

		// Find a split point inside the body (skip the ASCII header).
		const headerEnd = encoded.indexOf(0x0d, 0); // first \r
		const bodyStart = headerEnd + 4; // skip \r\n\r\n
		// Split in the middle of the body — likely inside a multi-byte char.
		const splitPoint = bodyStart + Math.floor((encoded.length - bodyStart) / 2);
		const chunk1 = encoded.slice(0, splitPoint);
		const chunk2 = encoded.slice(splitPoint);

		const decoder = new FrameDecoder();
		expect(decoder.decode(chunk1)).toHaveLength(0); // incomplete
		const result = decoder.decode(chunk2);
		expect(result).toHaveLength(1);
		expect(result[0]).toBe(msg);
	});

	it("handles Content-Length in bytes (not characters)", () => {
		// Content-Length counts bytes. For multi-byte content, byte length
		// > character length. The decoder must slice by bytes, not chars.
		const unicode = "🎉分段測試";
		const msg = JSON.stringify({ jsonrpc: "2.0", method: "test", params: { text: unicode } });
		const encoded = encode(msg);

		// Verify the Content-Length header matches the byte length of the body.
		const headerStr = new TextDecoder().decode(encoded.slice(0, encoded.indexOf(0x0d)));
		const contentLengthMatch = /Content-Length:\s*(\d+)/i.exec(headerStr);
		expect(contentLengthMatch).not.toBeNull();
		const declaredLength = Number.parseInt(contentLengthMatch?.[1], 10);
		const bodyBytes = new TextEncoder().encode(msg);
		expect(declaredLength).toBe(bodyBytes.length);

		const decoder = new FrameDecoder();
		const messages = decoder.decode(encoded);
		expect(messages).toHaveLength(1);
		expect(messages[0]).toBe(msg);
	});

	it("reassembles two multi-byte messages from one chunk", () => {
		const msg1 = JSON.stringify({ jsonrpc: "2.0", method: "a", params: { t: "日本語" } });
		const msg2 = JSON.stringify({ jsonrpc: "2.0", method: "b", params: { t: "한국어" } });
		const encoded1 = encode(msg1);
		const encoded2 = encode(msg2);

		const combined = new Uint8Array(encoded1.length + encoded2.length);
		combined.set(encoded1);
		combined.set(encoded2, encoded1.length);

		const decoder = new FrameDecoder();
		const messages = decoder.decode(combined);
		expect(messages).toHaveLength(2);
		expect(messages[0]).toBe(msg1);
		expect(messages[1]).toBe(msg2);
	});

	it("reassembles a multi-byte message split across three chunks", () => {
		const msg = JSON.stringify({
			jsonrpc: "2.0",
			method: "test",
			params: { text: "𝕳𝖊𝖑𝖑𝖔, 世界! Привет! 🌍" },
		});
		const encoded = encode(msg);

		const third = Math.floor(encoded.length / 3);
		const decoder = new FrameDecoder();
		expect(decoder.decode(encoded.slice(0, third))).toHaveLength(0);
		expect(decoder.decode(encoded.slice(third, third * 2))).toHaveLength(0);
		const result = decoder.decode(encoded.slice(third * 2));
		expect(result).toHaveLength(1);
		expect(result[0]).toBe(msg);
	});
});