stage2 更多的工具库
This commit is contained in:
1 parent
4141e7d574
commit
16a1f030d4
15 files changed
+1914
-109
No files matched your search
@@ -0,0 +1,184 @@
|
||||
// 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();
|
||||
}
|
||||
Reference in new issue
Block a user