const std = @import("std"); const session_mod = @import("session.zig"); // Tiny local session protocol for trusted same-user tools. // req: session/002, session/003, governance/003 test { _ = handleLine; } pub const max_request_bytes = 4096; pub fn handleLine(allocator: std.mem.Allocator, session: *session_mod.Session, line: []const u8) ![]u8 { const trimmed = std.mem.trimEnd(u8, line, "\r\n"); if (std.mem.eql(u8, trimmed, "state")) return stateResponse(allocator, session); if (std.mem.startsWith(u8, trimmed, "open ")) { const bytes = trimmed[5..]; if (!std.unicode.utf8ValidateSlice(bytes)) return allocator.dupe(u8, "err invalid utf8\n"); try session.openFixture(bytes); return stateResponse(allocator, session); } if (std.mem.startsWith(u8, trimmed, "command ")) { return commandResponse(allocator, session, trimmed[8..]); } return allocator.dupe(u8, "err unknown request\n"); } fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session, command: []const u8) ![]u8 { if (std.mem.eql(u8, command, "move_left")) return dispatchAndRespond(allocator, session, .move_left); if (std.mem.eql(u8, command, "move_right")) return dispatchAndRespond(allocator, session, .move_right); if (std.mem.eql(u8, command, "delete_backward")) return dispatchAndRespond(allocator, session, .delete_backward); if (std.mem.startsWith(u8, command, "insert ")) return dispatchAndRespond(allocator, session, .{ .insert = command[7..] }); if (std.mem.startsWith(u8, command, "panel_open ")) return panelRespond(allocator, session, .{ .open = command[11..] }); if (std.mem.startsWith(u8, command, "list_open ")) return listOpenRespond(allocator, session, command[10..]); if (std.mem.startsWith(u8, command, "list_filter ")) return listRespond(allocator, session, .{ .filter = command[12..] }); if (std.mem.eql(u8, command, "list_down")) return listRespond(allocator, session, .down); if (std.mem.eql(u8, command, "list_up")) return listRespond(allocator, session, .up); if (std.mem.eql(u8, command, "list_select")) return listRespond(allocator, session, .select); if (std.mem.eql(u8, command, "list_cancel")) return listRespond(allocator, session, .cancel); if (std.mem.eql(u8, command, "panel_close")) return panelRespond(allocator, session, .close); if (std.mem.eql(u8, command, "panel_next")) return panelRespond(allocator, session, .next); if (std.mem.eql(u8, command, "panel_prev")) return panelRespond(allocator, session, .previous); if (std.mem.startsWith(u8, command, "pair ")) { const pair = pairByName(command[5..]) orelse return allocator.dupe(u8, "err unknown pair\n"); return dispatchAndRespond(allocator, session, .{ .insert_pair = pair }); } return allocator.dupe(u8, "err unknown command\n"); } fn pairByName(name: []const u8) ?session_mod.Pair { if (std.mem.eql(u8, name, "parens")) return .{ .open = "(", .close = ")" }; if (std.mem.eql(u8, name, "brackets")) return .{ .open = "[", .close = "]" }; if (std.mem.eql(u8, name, "braces")) return .{ .open = "{", .close = "}" }; if (std.mem.eql(u8, name, "double_quote")) return .{ .open = "\"", .close = "\"" }; if (std.mem.eql(u8, name, "single_quote")) return .{ .open = "'", .close = "'" }; if (std.mem.eql(u8, name, "backtick")) return .{ .open = "`", .close = "`" }; return null; } fn dispatchAndRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: session_mod.Command) ![]u8 { session.dispatch(command) catch |err| switch (err) { error.InvalidUtf8Insertion => return allocator.dupe(u8, "err invalid utf8\n"), error.NoBufferOpen => return allocator.dupe(u8, "err no buffer open\n"), else => return err, }; return stateResponse(allocator, session); } const PanelCommand = union(enum) { open: []const u8, close, next, previous, }; fn panelRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: PanelCommand) ![]u8 { switch (command) { .open => |title| session.openPanel(title) catch |err| switch (err) { error.InvalidPanelTitle => return allocator.dupe(u8, "err invalid panel title\n"), else => return err, }, .close => session.closePanel() catch |err| switch (err) { error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"), else => return err, }, .next => session.nextPanel() catch |err| switch (err) { error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"), else => return err, }, .previous => session.previousPanel() catch |err| switch (err) { error.NoPanelOpen => return allocator.dupe(u8, "err no panel open\n"), else => return err, }, } return stateResponse(allocator, session); } const ListCommand = union(enum) { filter: []const u8, down, up, select, cancel, }; fn listOpenRespond(allocator: std.mem.Allocator, session: *session_mod.Session, payload: []const u8) ![]u8 { const split = std.mem.indexOfScalar(u8, payload, ' ') orelse return allocator.dupe(u8, "err invalid list open\n"); const title = payload[0..split]; const items_payload = payload[split + 1 ..]; var items = std.ArrayList([]const u8).empty; defer items.deinit(allocator); var iter = std.mem.splitScalar(u8, items_payload, '|'); while (iter.next()) |item| try items.append(allocator, item); session.openListPanel(title, items.items) catch |err| switch (err) { error.InvalidPanelTitle => return allocator.dupe(u8, "err invalid panel title\n"), error.InvalidListItem => return allocator.dupe(u8, "err invalid list item\n"), error.EmptyList => return allocator.dupe(u8, "err empty list\n"), else => return err, }; return stateResponse(allocator, session); } fn listRespond(allocator: std.mem.Allocator, session: *session_mod.Session, command: ListCommand) ![]u8 { switch (command) { .filter => |filter| session.filterListPanel(filter) catch |err| return listError(allocator, err), .down => session.listPanelDown() catch |err| return listError(allocator, err), .up => session.listPanelUp() catch |err| return listError(allocator, err), .select => _ = session.selectListPanel() catch |err| return listError(allocator, err), .cancel => session.cancelListPanel() catch |err| return listError(allocator, err), } return stateResponse(allocator, session); } fn listError(allocator: std.mem.Allocator, err: anyerror) ![]u8 { return switch (err) { error.NoPanelOpen => allocator.dupe(u8, "err no panel open\n"), error.ActivePanelIsNotList => allocator.dupe(u8, "err active panel is not list\n"), error.InvalidListFilter => allocator.dupe(u8, "err invalid list filter\n"), error.EmptyList => allocator.dupe(u8, "err empty list\n"), else => err, }; } fn stateResponse(allocator: std.mem.Allocator, session: *session_mod.Session) ![]u8 { const snap = try session.snapshot(); const panel_path = try session.panelPathAlloc(allocator); defer allocator.free(panel_path); const panel_summary = try session.panelSummaryAlloc(allocator); defer allocator.free(panel_summary); return std.fmt.allocPrint( allocator, "ok state cursor_byte={d} cursor_cell={d} bytes_len={d} panel_depth={d} active_panel={s} panel_path={s} panel_summary={s}\n", .{ snap.cursor_byte, snap.cursor_cell, snap.bytes.len, snap.panel_depth, if (snap.active_panel_title) |title| title else "-", panel_path, panel_summary, }, ); } test "regular: protocol opens bytes, reports state, and dispatches commands" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); const open = try handleLine(std.testing.allocator, &session, "open café\n"); defer std.testing.allocator.free(open); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", open); const moved = try handleLine(std.testing.allocator, &session, "command move_right\n"); defer std.testing.allocator.free(moved); try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=5 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", moved); const inserted = try handleLine(std.testing.allocator, &session, "command insert 🔥\n"); defer std.testing.allocator.free(inserted); try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=3 bytes_len=9 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", inserted); } test "regular: protocol delete command removes a whole UTF-8 codepoint" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); var response = try handleLine(std.testing.allocator, &session, "open aé\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command delete_backward\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=1 cursor_cell=1 bytes_len=1 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response); } test "adversarial: protocol rejects unknown requests and commands" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); const request = try handleLine(std.testing.allocator, &session, "subscribe everything\n"); defer std.testing.allocator.free(request); try std.testing.expectEqualStrings("err unknown request\n", request); try session.openFixture("abc"); const command = try handleLine(std.testing.allocator, &session, "command plugin.load\n"); defer std.testing.allocator.free(command); try std.testing.expectEqualStrings("err unknown command\n", command); } test "adversarial: protocol reports no buffer and invalid utf8 without corrupting state" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); const no_buffer = try handleLine(std.testing.allocator, &session, "state\n"); defer std.testing.allocator.free(no_buffer); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", no_buffer); try session.openFixture("safe"); const before = try session.snapshot(); const bad = [_]u8{ 'c', 'o', 'm', 'm', 'a', 'n', 'd', ' ', 'i', 'n', 's', 'e', 'r', 't', ' ', 0xc3, 0x28 }; const invalid = try handleLine(std.testing.allocator, &session, &bad); defer std.testing.allocator.free(invalid); try std.testing.expectEqualStrings("err invalid utf8\n", invalid); const after = try session.snapshot(); try std.testing.expectEqualStrings(before.bytes, after.bytes); } test "regular: protocol pair commands insert delimiters with cursor between them" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); var response = try handleLine(std.testing.allocator, &session, "open call\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command move_right\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command pair parens\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=5 cursor_cell=5 bytes_len=6 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response); try std.testing.expectEqualStrings("call()", (try session.snapshot()).bytes); } test "adversarial: protocol rejects unknown pair names" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); try session.openFixture("abc"); const response = try handleLine(std.testing.allocator, &session, "command pair snippet_everything\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err unknown pair\n", response); try std.testing.expectEqualStrings("abc", (try session.snapshot()).bytes); } test "regular: protocol opens switches and closes generic panels" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command panel_open files\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=empty\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_open diagnostics\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=diagnostics panel_path=files>[diagnostics] panel_summary=empty\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_prev\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=2 active_panel=files panel_path=[files]>diagnostics panel_summary=empty\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_close\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=diagnostics panel_path=[diagnostics] panel_summary=empty\n", response); } } test "adversarial: protocol rejects invalid panel titles and empty panel actions" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command panel_open bad title\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err invalid panel title\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command panel_close\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err no panel open\n", response); } } test "regular: protocol list panel filters moves selects and remains inspectable" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command list_open files src/main.zig|src/panel.zig|README.md\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=-,cursor=0,visible=3,selected=-\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=0,visible=2,selected=-\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_down\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=-\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_select\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=1 active_panel=files panel_path=[files] panel_summary=list:filter=src,cursor=1,visible=2,selected=src/panel.zig\n", response); } } test "regular: protocol list cancel closes list panel" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); var response = try handleLine(std.testing.allocator, &session, "command list_open files a.zig|b.zig\n"); std.testing.allocator.free(response); response = try handleLine(std.testing.allocator, &session, "command list_cancel\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("ok state cursor_byte=0 cursor_cell=0 bytes_len=0 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response); } test "adversarial: protocol list errors are explicit" { var session = session_mod.Session.init(std.testing.allocator); defer session.deinit(); { const response = try handleLine(std.testing.allocator, &session, "command list_open files bad item\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err invalid list item\n", response); } { const response = try handleLine(std.testing.allocator, &session, "command list_filter src\n"); defer std.testing.allocator.free(response); try std.testing.expectEqualStrings("err no panel open\n", response); } }