185 lines
5.9 KiB
Plaintext
185 lines
5.9 KiB
Plaintext
// Expect retcode = 0
|
|
// LSP 传输帧:Content-Length 头 + 空行 + 正文。
|
|
// 这里只做纯逻辑(写入/解析 ByteBuffer/ByteView),真正的 stdin/stdout I/O 由上层负责。
|
|
|
|
type .{ .Error, .Status, .make } = @import("error.spl");
|
|
type .{ .ByteView } = @import("byte_view.spl");
|
|
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
|
type .{ .append_usize } = @import("fmt.spl");
|
|
|
|
type FrameResult = enum {
|
|
Ok: usize, // 消耗的字节数
|
|
Incomplete,
|
|
Err: Error,
|
|
}
|
|
|
|
fn ascii_lower(c: u8): u8 {
|
|
if c >= 'A' && c <= 'Z' { ret c + ('a' - 'A'); }
|
|
ret c;
|
|
}
|
|
|
|
// data[j..] 是否以 word 开头(ASCII 大小写不敏感)。
|
|
fn ci_prefix(data: *u8, len: usize, j: usize, word: []u8): bool {
|
|
if j + word.len > len { ret false; }
|
|
var i: usize = 0;
|
|
while i < word.len {
|
|
if ascii_lower(data[j + i]) != ascii_lower(word.ptr[i]) { ret false; }
|
|
i = i + 1;
|
|
}
|
|
ret true;
|
|
}
|
|
|
|
fn write_frame(out: *ByteBuffer, body: []u8): Status {
|
|
match ByteBuffer.append(out, "Content-Length: ") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
|
match append_usize(out, body.len, 10, false) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
|
match ByteBuffer.append(out, "\r\n\r\n") { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
|
ret ByteBuffer.append(out, body);
|
|
}
|
|
|
|
// 从 input 解析一个帧;正文复制到 body_out。缺数据返回 Incomplete。
|
|
fn parse_frame(input: ByteView, body_out: *ByteBuffer): FrameResult {
|
|
var data = ByteView.as_slice(input);
|
|
var len = data.len;
|
|
|
|
// 找头结束标记 \r\n\r\n
|
|
var term = ~(0 as usize);
|
|
var i: usize = 0;
|
|
while i + 3 < len {
|
|
if data[i] == '\r' && data[i + 1] == '\n' && data[i + 2] == '\r' && data[i + 3] == '\n' {
|
|
term = i;
|
|
break;
|
|
}
|
|
i = i + 1;
|
|
}
|
|
if term == ~(0 as usize) { ret .Incomplete; }
|
|
|
|
// 在头内找 Content-Length(大小写不敏感)
|
|
var content_len: usize = 0;
|
|
var found = false;
|
|
var j: usize = 0;
|
|
while j + 15 <= term {
|
|
if ci_prefix(data.ptr, term, j, "content-length:") {
|
|
var k = j + 15;
|
|
while k < term && (data[k] == ' ' || data[k] == '\t') { k = k + 1; }
|
|
var n: usize = 0;
|
|
var digits = 0;
|
|
while k < term && data[k] >= '0' && data[k] <= '9' {
|
|
n = n * 10 + (data[k] - '0') as usize;
|
|
digits = digits + 1;
|
|
k = k + 1;
|
|
}
|
|
if digits == 0 { ret .Err(make(.InvalidDigit)); }
|
|
content_len = n;
|
|
found = true;
|
|
break;
|
|
}
|
|
j = j + 1;
|
|
}
|
|
if !found { ret .Err(make(.InvalidFormat)); }
|
|
|
|
var body_start = term + 4;
|
|
if len < body_start || content_len > len - body_start { ret .Incomplete; }
|
|
|
|
ByteBuffer.clear(body_out);
|
|
var body = data[body_start..body_start + content_len];
|
|
match ByteBuffer.append(body_out, body) { .Ok => {}, .Err(e) => { ret .Err(e); } }
|
|
ret .Ok(body_start + content_len);
|
|
}
|
|
|
|
// ---- 测试辅助 ----
|
|
|
|
fn expect_ok(result: Status) {
|
|
match result {
|
|
.Ok => {},
|
|
.Err(error) => { @assert(false, "unexpected frame error"); },
|
|
}
|
|
}
|
|
|
|
fn expect_body(out: *ByteBuffer, expected: []u8) {
|
|
var actual = ByteBuffer.as_slice(out.*);
|
|
@assert(actual.len == expected.len, "body length");
|
|
var i: usize = 0;
|
|
while i < expected.len {
|
|
@assert(actual.ptr[i] == expected.ptr[i], "body byte");
|
|
i = i + 1;
|
|
}
|
|
}
|
|
|
|
fn test_roundtrip() {
|
|
var wire = ByteBuffer.new();
|
|
expect_ok(write_frame(&wire, "{\"jsonrpc\":\"2.0\"}"));
|
|
var bytes = ByteBuffer.as_slice(wire);
|
|
@assert(bytes.len == 16 + 2 + 4 + 17, "wire length"); // "Content-Length: 17\r\n\r\n" + body
|
|
|
|
var body = ByteBuffer.new();
|
|
match parse_frame(ByteView.from_slice(bytes), &body) {
|
|
.Ok(used) => {
|
|
@assert(used == bytes.len, "consumed all");
|
|
expect_body(&body, "{\"jsonrpc\":\"2.0\"}");
|
|
},
|
|
.Incomplete => { @assert(false, "unexpected incomplete"); },
|
|
.Err(error) => { @assert(false, "unexpected err"); },
|
|
}
|
|
ByteBuffer.drop(&body);
|
|
ByteBuffer.drop(&wire);
|
|
}
|
|
|
|
fn test_multiple_and_leftover() {
|
|
var wire = ByteBuffer.new();
|
|
expect_ok(write_frame(&wire, "abcd"));
|
|
expect_ok(write_frame(&wire, "ef"));
|
|
var bytes = ByteBuffer.as_slice(wire);
|
|
|
|
var body = ByteBuffer.new();
|
|
match parse_frame(ByteView.from_slice(bytes), &body) {
|
|
.Ok(used) => {
|
|
expect_body(&body, "abcd");
|
|
var rest = bytes[used..bytes.len];
|
|
match parse_frame(ByteView.from_slice(rest), &body) {
|
|
.Ok(used2) => { expect_body(&body, "ef"); @assert(used2 == rest.len, "second"); },
|
|
_ => { @assert(false, "second frame"); },
|
|
}
|
|
},
|
|
_ => { @assert(false, "first frame"); },
|
|
}
|
|
ByteBuffer.drop(&body);
|
|
ByteBuffer.drop(&wire);
|
|
}
|
|
|
|
fn test_incomplete_and_errors() {
|
|
var body = ByteBuffer.new();
|
|
|
|
// 头未结束
|
|
var h1 = "Content-Length: 3\r\n";
|
|
match parse_frame(ByteView.from_slice(h1), &body) {
|
|
.Incomplete => {},
|
|
_ => { @assert(false, "h1"); },
|
|
}
|
|
// 正文不足
|
|
var h2 = "Content-Length: 3\r\n\r\nab";
|
|
match parse_frame(ByteView.from_slice(h2), &body) {
|
|
.Incomplete => {},
|
|
_ => { @assert(false, "h2"); },
|
|
}
|
|
// 缺 Content-Length
|
|
var h3 = "X: 1\r\n\r\n";
|
|
match parse_frame(ByteView.from_slice(h3), &body) {
|
|
.Ok(used) => { @assert(false, "h3 ok"); },
|
|
.Incomplete => { @assert(false, "h3 incomplete"); },
|
|
.Err(error) => {},
|
|
}
|
|
// 大小写不敏感 + 额外头
|
|
var h4 = "X-Test: yes\r\ncontent-length: 2\r\n\r\nhi";
|
|
match parse_frame(ByteView.from_slice(h4), &body) {
|
|
.Ok(used) => { expect_body(&body, "hi"); },
|
|
_ => { @assert(false, "h4"); },
|
|
}
|
|
ByteBuffer.drop(&body);
|
|
}
|
|
|
|
fn main() {
|
|
test_roundtrip();
|
|
test_multiple_and_leftover();
|
|
test_incomplete_and_errors();
|
|
}
|