645 lines · 21.7 KB · Zig
RawDownload
const std = @import("std");const io_mod = @import("../core/shared/io.zig");const host_target = @import("../core/hosts/target.zig"); const Allocator = std.mem.Allocator; pub fn writeJsonStr(s: []const u8, w: *std.Io.Writer) !void {    try w.writeByte('"');    for (s) |c| {        switch (c) {            '"' => try w.writeAll("\\\""),            '\\' => try w.writeAll("\\\\"),            '\n' => try w.writeAll("\\n"),            '\r' => try w.writeAll("\\r"),            '\t' => try w.writeAll("\\t"),            0x08 => try w.writeAll("\\b"),            0x0c => try w.writeAll("\\f"),            else => {                if (c < 0x20) {                    try w.print("\\u{x:0>4}", .{c});                } else {                    try w.writeByte(c);                }            },        }    }    try w.writeByte('"');} pub const ErrorCode = struct {    pub const request_frame_too_large: i64 = -32000;    pub const parse_error: i64 = -32700;    pub const invalid_request: i64 = -32600;    pub const method_not_found: i64 = -32601;    pub const invalid_params: i64 = -32602;    pub const internal_error: i64 = -32603;}; pub const RequestId = union(enum) {    integer: i64,    string: []const u8,    null,}; pub const Message = struct {    id: ?RequestId = null,    method: []const u8 = "",    params_raw: ?[]const u8 = null,    result_raw: ?[]const u8 = null,    error_raw: ?[]const u8 = null,     pub fn isResponse(self: Message) bool {        return self.method.len == 0 and self.id != null and            (self.result_raw != null or self.error_raw != null);    }}; pub const RpcError = struct {    code: i64,    message: []const u8,}; pub fn parseMessage(alloc: Allocator, line: []const u8) !Message {    const parsed = std.json.parseFromSlice(std.json.Value, alloc, line, .{}) catch        return error.ParseError;    defer parsed.deinit();     const root = parsed.value;    if (root != .object) return error.InvalidRequest;     var msg = Message{};    errdefer freeMessage(alloc, &msg);     if (root.object.get("id")) |id_val| {        switch (id_val) {            .integer => |n| msg.id = .{ .integer = n },            .string => |s| msg.id = .{ .string = try alloc.dupe(u8, s) },            .null => msg.id = .null,            else => return error.InvalidRequest,        }    }     if (root.object.get("method")) |method_val| {        switch (method_val) {            .string => |s| msg.method = try alloc.dupe(u8, s),            else => return error.InvalidRequest,        }    }     if (root.object.get("params")) |params_val| {        msg.params_raw = try stringifyValueAlloc(alloc, params_val);    }     if (root.object.get("result")) |result_val| {        msg.result_raw = try stringifyValueAlloc(alloc, result_val);    }    if (root.object.get("error")) |error_val| {        msg.error_raw = try stringifyValueAlloc(alloc, error_val);    }    if (msg.result_raw != null and msg.error_raw != null) return error.InvalidRequest;    if (msg.method.len > 0 and (msg.result_raw != null or msg.error_raw != null)) return error.InvalidRequest;    if (msg.method.len == 0 and !msg.isResponse()) return error.InvalidRequest;     return msg;} fn stringifyValueAlloc(alloc: Allocator, value: std.json.Value) ![]u8 {    var out: std.Io.Writer.Allocating = .init(alloc);    defer out.deinit();    try std.json.Stringify.value(value, .{}, &out.writer);    return try out.toOwnedSlice();} pub fn freeMessage(alloc: Allocator, msg: *Message) void {    if (msg.method.len > 0) alloc.free(msg.method);    if (msg.params_raw) |p| alloc.free(p);    if (msg.result_raw) |p| alloc.free(p);    if (msg.error_raw) |p| alloc.free(p);    if (msg.id) |id| {        switch (id) {            .string => |s| alloc.free(s),            else => {},        }    }    msg.* = .{};} pub const Writer = struct {    pub const WriteFn = *const fn (?*anyopaque, []const u8) anyerror!void;     const Callback = struct {        context: ?*anyopaque,        write_fn: WriteFn,    };     mutex: std.Io.Mutex = .init,    stdout: std.Io.File,    callback: ?Callback = null,     const Frame = union(enum) {        response: struct {            id: ?RequestId,            result_json: []const u8,        },        error_response: struct {            id: ?RequestId,            err: RpcError,        },        notification: struct {            method: []const u8,            params_json: []const u8,        },        request: struct {            id: RequestId,            method: []const u8,            params_json: []const u8,        },         fn write(self: Frame, w: *std.Io.Writer) !void {            switch (self) {                .response => |response| {                    try w.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");                    try writeId(w, response.id);                    try w.writeAll(",\"result\":");                    try w.writeAll(response.result_json);                    try w.writeAll("}\n");                },                .error_response => |response| {                    try w.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");                    try writeId(w, response.id);                    try w.writeAll(",\"error\":{\"code\":");                    try w.print("{d}", .{response.err.code});                    try w.writeAll(",\"message\":");                    try writeJsonStr(response.err.message, w);                    try w.writeAll("}}\n");                },                .notification => |notification| {                    try w.writeAll("{\"jsonrpc\":\"2.0\",\"method\":");                    try writeJsonStr(notification.method, w);                    try w.writeAll(",\"params\":");                    try w.writeAll(notification.params_json);                    try w.writeAll("}\n");                },                .request => |request| {                    try w.writeAll("{\"jsonrpc\":\"2.0\",\"id\":");                    try writeId(w, request.id);                    try w.writeAll(",\"method\":");                    try writeJsonStr(request.method, w);                    try w.writeAll(",\"params\":");                    try w.writeAll(request.params_json);                    try w.writeAll("}\n");                },            }        }    };     pub fn init() Writer {        return .{ .stdout = std.Io.File.stdout() };    }     pub fn initCallback(context: ?*anyopaque, write_fn: WriteFn) Writer {        return .{            .stdout = std.Io.File.stdout(),            .callback = .{                .context = context,                .write_fn = write_fn,            },        };    }     pub fn writeResponse(self: *Writer, alloc: Allocator, id: ?RequestId, result_json: []const u8) !void {        try self.writeFrame(alloc, .{ .response = .{            .id = id,            .result_json = result_json,        } });    }     pub fn writeError(self: *Writer, alloc: Allocator, id: ?RequestId, err: RpcError) !void {        try self.writeFrame(alloc, .{ .error_response = .{            .id = id,            .err = err,        } });    }     pub fn writeNotification(self: *Writer, alloc: Allocator, method: []const u8, params_json: []const u8) !void {        try self.writeFrame(alloc, .{ .notification = .{            .method = method,            .params_json = params_json,        } });    }     pub fn writeRequest(self: *Writer, alloc: Allocator, id: RequestId, method: []const u8, params_json: []const u8) !void {        try self.writeFrame(alloc, .{ .request = .{            .id = id,            .method = method,            .params_json = params_json,        } });    }     fn writeFrame(self: *Writer, alloc: Allocator, frame: Frame) !void {        var out: std.Io.Writer.Allocating = .init(alloc);        defer out.deinit();         try frame.write(&out.writer);         const slice = try out.toOwnedSlice();        defer alloc.free(slice);         const io = io_mod.getIo();        self.mutex.lockUncancelable(io);        defer self.mutex.unlock(io);        if (self.callback) |callback| {            try callback.write_fn(callback.context, slice);        } else {            try self.stdout.writeStreamingAll(io, slice);        }    }     fn writeId(w: *std.Io.Writer, id: ?RequestId) !void {        if (id) |rid| {            switch (rid) {                .integer => |n| try w.print("{d}", .{n}),                .string => |s| try writeJsonStr(s, w),                .null => try w.writeAll("null"),            }        } else {            try w.writeAll("null");        }    }}; pub const Reader = struct {    pub const ReadFn = *const fn (?*anyopaque, []u8) usize;     pub const frame_resource_byte_limit: usize = 8 * 1024 * 1024;    pub const LineRead = union(enum) {        line: []u8,        overflow,    };     read_context: ?*anyopaque = null,    read_fn: ReadFn,    buf: [64 * 1024]u8 = undefined,    pos: usize = 0,    len: usize = 0,     pub fn init() Reader {        return .{ .read_fn = readStdin };    }     pub fn initCallback(context: ?*anyopaque, read_fn: ReadFn) Reader {        return .{            .read_context = context,            .read_fn = read_fn,        };    }     fn initForTesting(context: *anyopaque, read_fn: ReadFn) Reader {        return initCallback(context, read_fn);    }     // Native ACP uses raw read(2) because Linux may pass socket-based stdin to    // child processes. WASI has no std.posix surface, so use the injected std.Io    // backend and let the host's fd_read import suspend through JSPI.    fn readStdin(_: ?*anyopaque, destination: []u8) usize {        if (comptime host_target.is_wasm) {            return std.Io.File.stdin().readStreaming(                io_mod.getIo(),                &.{destination},            ) catch return 0;        }        return std.posix.read(std.posix.STDIN_FILENO, destination) catch return 0;    }     fn readFromSource(self: *Reader) usize {        return self.read_fn(self.read_context, &self.buf);    }     pub fn readLine(self: *Reader, alloc: Allocator) !?LineRead {        return self.readLineWithLimit(alloc, frame_resource_byte_limit);    }     fn readLineWithLimit(        self: *Reader,        alloc: Allocator,        resource_byte_limit: usize,    ) !?LineRead {        var line_buf: std.ArrayList(u8) = .empty;        defer line_buf.deinit(alloc);        var overflow = false;         while (true) {            if (self.findNewline()) |nl_pos| {                if (!overflow and !try appendFrameChunk(                    alloc,                    &line_buf,                    self.buf[self.pos .. self.pos + nl_pos],                    resource_byte_limit,                )) {                    overflow = true;                    line_buf.clearAndFree(alloc);                }                self.pos += nl_pos + 1;                if (overflow) return .overflow;                if (line_buf.items.len == 0) continue;                return .{ .line = try line_buf.toOwnedSlice(alloc) };            }             if (!overflow and self.pos < self.len and !try appendFrameChunk(                alloc,                &line_buf,                self.buf[self.pos..self.len],                resource_byte_limit,            )) {                overflow = true;                line_buf.clearAndFree(alloc);            }            self.pos = 0;            self.len = self.readFromSource();            if (self.len == 0) return if (overflow) .overflow else null;        }    }     fn appendFrameChunk(        alloc: Allocator,        line_buf: *std.ArrayList(u8),        chunk: []const u8,        resource_byte_limit: usize,    ) !bool {        const next_len = std.math.add(usize, line_buf.items.len, chunk.len) catch            return false;        if (next_len > resource_byte_limit) return false;        try line_buf.appendSlice(alloc, chunk);        return true;    }     fn findNewline(self: *Reader) ?usize {        for (self.buf[self.pos..self.len], 0..) |c, i| {            if (c == '\n') return i;        }        return null;    }}; const ChunkedTestSource = struct {    bytes: []const u8,    offset: usize = 0,    max_chunk: usize,     fn read(raw: ?*anyopaque, destination: []u8) usize {        const self: *ChunkedTestSource = @ptrCast(@alignCast(raw.?));        if (self.offset == self.bytes.len) return 0;        const remaining = self.bytes.len - self.offset;        const count = @min(@min(remaining, destination.len), self.max_chunk);        @memcpy(destination[0..count], self.bytes[self.offset .. self.offset + count]);        self.offset += count;        return count;    }}; test "Reader accepts an exact fragmented frame" {    const alloc = std.testing.allocator;    var source = ChunkedTestSource{        .bytes = "12345678\n",        .max_chunk = 2,    };    var reader = Reader.initForTesting(&source, ChunkedTestSource.read);    const result = (try reader.readLineWithLimit(alloc, 8)).?;    switch (result) {        .line => |line| {            defer alloc.free(line);            try std.testing.expectEqualStrings("12345678", line);        },        .overflow => return error.TestUnexpectedResult,    }} test "Reader drains one oversized fragmented frame and resumes at the next frame" {    const alloc = std.testing.allocator;    var source = ChunkedTestSource{        .bytes = "123456789\n{\"id\":2}\n",        .max_chunk = 3,    };    var reader = Reader.initForTesting(&source, ChunkedTestSource.read);     const overflow = (try reader.readLineWithLimit(alloc, 8)).?;    try std.testing.expect(overflow == .overflow);     const next = (try reader.readLineWithLimit(alloc, 8)).?;    switch (next) {        .line => |line| {            defer alloc.free(line);            try std.testing.expectEqualStrings("{\"id\":2}", line);        },        .overflow => return error.TestUnexpectedResult,    }} test "Reader reports one overflow when an oversized frame ends at EOF" {    const alloc = std.testing.allocator;    var source = ChunkedTestSource{        .bytes = "123456789",        .max_chunk = 2,    };    var reader = Reader.initForTesting(&source, ChunkedTestSource.read);    try std.testing.expect((try reader.readLineWithLimit(alloc, 8)).? == .overflow);    try std.testing.expect((try reader.readLineWithLimit(alloc, 8)) == null);} test "Reader propagates allocation failure without publishing a partial frame" {    var source = ChunkedTestSource{        .bytes = "12345678\n",        .max_chunk = 2,    };    var reader = Reader.initForTesting(&source, ChunkedTestSource.read);    var failing = std.testing.FailingAllocator.init(        std.testing.allocator,        .{ .fail_index = 0 },    );    try std.testing.expectError(        error.OutOfMemory,        reader.readLineWithLimit(failing.allocator(), 8),    );} test "parseMessage parses request with integer id" {    const alloc = std.testing.allocator;    var msg = try parseMessage(alloc, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":1}}");    defer freeMessage(alloc, &msg);     try std.testing.expectEqualStrings("initialize", msg.method);    try std.testing.expect(msg.id != null);    switch (msg.id.?) {        .integer => |n| try std.testing.expectEqual(@as(i64, 1), n),        else => return error.TestUnexpectedResult,    }    try std.testing.expect(msg.params_raw != null);} test "parseMessage parses notification without id" {    const alloc = std.testing.allocator;    var msg = try parseMessage(alloc, "{\"jsonrpc\":\"2.0\",\"method\":\"session/cancel\",\"params\":{\"sessionId\":\"abc\"}}");    defer freeMessage(alloc, &msg);     try std.testing.expectEqualStrings("session/cancel", msg.method);    try std.testing.expect(msg.id == null);} test "parseMessage rejects messages without a method or response payload" {    const alloc = std.testing.allocator;    const result = parseMessage(alloc, "{\"jsonrpc\":\"2.0\",\"id\":1}");    try std.testing.expectError(error.InvalidRequest, result);} test "parseMessage accepts correlated client responses" {    const alloc = std.testing.allocator;    var msg = try parseMessage(alloc, "{\"jsonrpc\":\"2.0\",\"id\":7,\"result\":{\"outcome\":{\"outcome\":\"selected\",\"optionId\":\"allow_once\"}}}");    defer freeMessage(alloc, &msg);    try std.testing.expect(msg.isResponse());    try std.testing.expect(msg.result_raw != null);

Lines 1–500 of 645.