// transport — LSP base-protocol framing: Content-Length headers over a // byte stream. // // The Language Server Protocol wraps every JSON-RPC message in an // HTTP-style header block — `Content-Length: N\r\n`, optionally other // headers, then `\r\n` and exactly N body bytes. This module is the pure // half of the server's stdio loop: `scan` recognizes one complete frame // in a byte buffer, `frame` wraps a body for writing, and `Decoder` // accumulates arbitrary read chunks and hands out complete bodies in // order. Nothing here touches a file descriptor — the tests feed byte // buffers, and the server owns the actual stdin/stdout plumbing. // // Per the spec, header names compare case-insensitively and unknown // headers (`Content-Type`) are skipped. A complete header block without // a parseable Content-Length is `error.Malformed` — with the framing // gone, the stream can never resynchronize, so the server's only move // is to report and exit. use std use core pub const Error = error{ Malformed, OutOfMemory } // One recognized frame: the body spans `[body_start, body_start+body_len)` // and the whole frame — headers included — occupies the first `total` // bytes of the scanned buffer. pub const Scan = struct { body_start usize, body_len usize, total usize, } // Recognize one complete frame at the start of `buf`. Returns null while // the frame is still incomplete (no header terminator yet, or fewer than // Content-Length body bytes buffered) — feed more bytes and rescan. pub fn scan(buf []u8) Error!?Scan { term := core.mem.indexOf(u8, buf, "\r\n\r\n") orelse return null var body_len ?usize = null var pos usize = 0 while pos < term { var line_end = term if core.mem.indexOf(u8, buf[pos..term], "\r\n") |rel| { line_end = pos + rel } line := buf[pos..line_end] colon := core.mem.indexOfScalar(u8, line, ':') orelse return error.Malformed if asciiEqlNoCase(line[0..colon], "content-length") { v := trim(line[colon + 1 ..]) body_len = core.fmt.parseInt(usize, v, 10) catch return error.Malformed } pos = line_end + 2 } n := body_len orelse return error.Malformed body_start := term + 4 if buf.len < body_start + n { return null } return Scan{ .body_start = body_start, .body_len = n, .total = body_start + n } } // Wrap `body` in its header, freshly allocated: the exact bytes to write // to the protocol channel. pub fn frame(alloc std.mem.Allocator, body []u8) Error![]u8 { return core.fmt.allocPrint(alloc, "Content-Length: {d}\r\n\r\n{s}", .{ body.len, body }) } // The accumulating reader side: `feed` appends whatever chunk arrived, // `next` hands out the body of the first complete frame. The returned // slice points into the decoder's buffer and stays valid until the next // `feed` — handle the message (or copy it out) before reading again. pub const Decoder = struct { // The allocation; live bytes are `data[start..len]`. `start` marks // bytes already handed out by `next`, reclaimed on the next `feed`. data []mut u8, len usize, start usize, pub const empty Decoder = .{ .data = &.{}, .len = 0, .start = 0 } pub fn deinit(self *mut Decoder, alloc std.mem.Allocator) void { alloc.free(self.data) self.* = .empty } // Append a read chunk, compacting consumed bytes away first so the // buffer never grows past one frame plus one read. pub fn feed(self *mut Decoder, alloc std.mem.Allocator, bytes []u8) Error!void { if self.start > 0 { n := self.len - self.start core.mem.copy(u8, self.data[0..n], self.data[self.start..self.len]) self.len = n self.start = 0 } needed := self.len + bytes.len if needed > self.data.len { var cap usize = self.data.len * 2 if cap < 64 { cap = 64 } if cap < needed { cap = needed } grown := try alloc.alloc(u8, cap) core.mem.copy(u8, grown[0..self.len], self.data[0..self.len]) alloc.free(self.data) self.data = grown } core.mem.copy(u8, self.data[self.len .. self.len + bytes.len], bytes) self.len += bytes.len } // The body of the next complete frame, or null when more bytes are // needed. Valid until the next `feed`. pub fn next(self *mut Decoder) Error!?[]u8 { live := self.data[self.start..self.len] s := (try scan(live)) orelse return null body := live[s.body_start .. s.body_start + s.body_len] self.start += s.total return body } } fn asciiEqlNoCase(a []u8, b []u8) bool { if a.len != b.len { return false } for c, i in a { if asciiLower(c) != asciiLower(b[i]) { return false } } return true } fn asciiLower(c u8) u8 { if c >= 'A' && c <= 'Z' { return c + 32 } return c } fn trim(s []u8) []u8 { var lo usize = 0 var hi usize = s.len while lo < hi && (s[lo] == ' ' || s[lo] == '\t') { lo += 1 } while hi > lo && (s[hi - 1] == ' ' || s[hi - 1] == '\t') { hi -= 1 } return s[lo..hi] } test "scan recognizes a complete frame" { s := (try scan("Content-Length: 2\r\n\r\nhi")).? try std.testing.expectEqual(21, s.body_start) try std.testing.expectEqual(2, s.body_len) try std.testing.expectEqual(23, s.total) } test "scan waits for the header terminator and the full body" { try std.testing.expect((try scan("")) == null) try std.testing.expect((try scan("Content-Length: 2")) == null) try std.testing.expect((try scan("Content-Length: 2\r\n\r\n")) == null) try std.testing.expect((try scan("Content-Length: 2\r\n\r\nh")) == null) } test "scan skips unknown headers and compares names case-insensitively" { msg := "content-LENGTH: 4\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\nbody" s := (try scan(msg)).? try std.testing.expectEqual(4, s.body_len) try std.testing.expectEqual(msg.len, s.total) } test "scan rejects a header block it cannot frame by" { try std.testing.expectError(error.Malformed, scan("Content-Type: text\r\n\r\nxx")) try std.testing.expectError(error.Malformed, scan("Content-Length: ten\r\n\r\nxx")) try std.testing.expectError(error.Malformed, scan("no colon here\r\n\r\nxx")) } test "frame writes the exact bytes scan reads back" { var a = core.arena.ArenaAllocator.init(std.testing.allocator) defer a.deinit() f := try frame(a.allocator(), "{\"id\":1}") try std.testing.expect(core.mem.eql(u8, f, "Content-Length: 8\r\n\r\n{\"id\":1}")) s := (try scan(f)).? try std.testing.expect(core.mem.eql(u8, f[s.body_start .. s.body_start + s.body_len], "{\"id\":1}")) } test "a decoder fed one byte at a time yields every message in order" { var d Decoder = .empty defer d.deinit(std.testing.allocator) stream := "Content-Length: 3\r\n\r\noneContent-Length: 3\r\n\r\ntwo" var got usize = 0 for c in stream { one := [1]u8{c} try d.feed(std.testing.allocator, one[0..]) while true { body := (try d.next()) orelse break if got == 0 { try std.testing.expect(core.mem.eql(u8, body, "one")) } else { try std.testing.expect(core.mem.eql(u8, body, "two")) } got += 1 } } try std.testing.expectEqual(2, got) } test "a decoder hands out back-to-back frames from one feed" { var d Decoder = .empty defer d.deinit(std.testing.allocator) try d.feed(std.testing.allocator, "Content-Length: 1\r\n\r\naContent-Length: 1\r\n\r\nb") try std.testing.expect(core.mem.eql(u8, (try d.next()).?, "a")) try std.testing.expect(core.mem.eql(u8, (try d.next()).?, "b")) try std.testing.expect((try d.next()) == null) } fn feedSweep(alloc std.mem.Allocator) !void { var d Decoder = .empty defer d.deinit(alloc) try d.feed(alloc, "Content-Length: 3\r\n\r\nxyz") body := (try d.next()).? try std.testing.expect(core.mem.eql(u8, body, "xyz")) } test "feed survives failure at every allocation point" { try std.testing.checkAllAllocationFailures(std.testing.allocator, feedSweep, .{}) }