Daemon: parse requests with kotlinx.serialization and read fields from
the params object instead of scanning the whole line for key markers,
which picked up decoy keys ("path", "version", "id") occurring inside
buffer text. Build all responses and events with JsonObject builders,
which also fixes unescaped control characters in output and \uXXXX
escapes in input. Malformed lines now get a BAD_REQUEST reply instead
of corrupting field extraction.
Client: quote outgoing JSON strings with a strict json_quote helper
instead of Odin's %q verb, whose \x/\e escapes are not valid JSON.
Verified end to end: adversarial payloads (decoy protocol keys,
unicode, control chars, malformed lines) against the daemon, and the
full client smoke flow (ids 1-10) against a real Gradle workspace.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
184 lines
5.7 KiB
Odin
184 lines
5.7 KiB
Odin
package main
|
|
|
|
import "core:fmt"
|
|
import "core:net"
|
|
import "core:os"
|
|
import "core:strconv"
|
|
|
|
main :: proc() {
|
|
args := os.args
|
|
if len(args) < 2 {
|
|
fmt.println("usage: odin run client/odin -- <port> [workspace]")
|
|
fmt.println(" or: odin run client/odin -- --sdl [workspace] [port] [file]")
|
|
return
|
|
}
|
|
|
|
if args[1] == "--sdl" {
|
|
workspace := "."
|
|
if len(args) >= 3 {
|
|
workspace = args[2]
|
|
}
|
|
port := 0
|
|
file := ""
|
|
if len(args) >= 4 {
|
|
parsed_port, parsed_ok := strconv.parse_int(args[3])
|
|
if parsed_ok {
|
|
port = int(parsed_port)
|
|
} else {
|
|
file = args[3]
|
|
}
|
|
}
|
|
if len(args) >= 5 {
|
|
file = args[4]
|
|
}
|
|
run_sdl_editor(workspace, port, file)
|
|
return
|
|
}
|
|
|
|
port, ok := strconv.parse_int(args[1])
|
|
if !ok {
|
|
fmt.println("invalid port")
|
|
return
|
|
}
|
|
|
|
workspace := "."
|
|
if len(args) >= 3 {
|
|
workspace = args[2]
|
|
}
|
|
|
|
run_buffer_smoke()
|
|
editor := run_editor_smoke(workspace)
|
|
defer editor_destroy(&editor)
|
|
|
|
socket, err := net.dial_tcp("127.0.0.1", int(port))
|
|
if err != nil {
|
|
fmt.println("connect failed:", err)
|
|
return
|
|
}
|
|
defer net.close(socket)
|
|
|
|
send_line(socket, "{\"id\":1,\"method\":\"ping\",\"params\":{}}\n")
|
|
read_messages(socket, 1)
|
|
|
|
open_workspace := fmt.tprintf("{{\"id\":2,\"method\":\"workspace/open\",\"params\":{{\"root\":%s}}}}\n", json_quote(workspace))
|
|
send_line(socket, open_workspace)
|
|
read_messages(socket, 3)
|
|
|
|
send_line(socket, "{\"id\":3,\"method\":\"gradle/tasks\",\"params\":{}}\n")
|
|
read_messages(socket, 1)
|
|
|
|
active := editor_active_buffer(&editor)
|
|
diagnostics_path := active.path
|
|
active_text := editor_active_text(&editor)
|
|
defer delete(active_text)
|
|
|
|
text_change_request := fmt.tprintf("{{\"id\":4,\"method\":\"text/change\",\"params\":{{\"path\":%s,\"version\":1,\"text\":%s}}}}\n", json_quote(diagnostics_path), json_quote(string(active_text[:])))
|
|
send_line(socket, text_change_request)
|
|
read_messages(socket, 2)
|
|
|
|
diagnostics_request := fmt.tprintf("{{\"id\":5,\"method\":\"kotlin/diagnostics\",\"params\":{{\"path\":%s}}}}\n", json_quote(diagnostics_path))
|
|
send_line(socket, diagnostics_request)
|
|
read_messages(socket, 1)
|
|
|
|
completion_request := fmt.tprintf("{{\"id\":6,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%s,\"line\":1,\"column\":1}}}}\n", json_quote(diagnostics_path))
|
|
send_line(socket, completion_request)
|
|
read_messages(socket, 1)
|
|
|
|
hover_request := fmt.tprintf("{{\"id\":7,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%s,\"line\":1,\"column\":1}}}}\n", json_quote(diagnostics_path))
|
|
send_line(socket, hover_request)
|
|
read_messages(socket, 1)
|
|
|
|
definition_request := fmt.tprintf("{{\"id\":8,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%s,\"line\":1,\"column\":5}}}}\n", json_quote(diagnostics_path))
|
|
send_line(socket, definition_request)
|
|
read_messages(socket, 1)
|
|
|
|
references_request := fmt.tprintf("{{\"id\":9,\"method\":\"kotlin/references\",\"params\":{{\"path\":%s,\"line\":1,\"column\":5}}}}\n", json_quote(diagnostics_path))
|
|
send_line(socket, references_request)
|
|
read_messages(socket, 1)
|
|
|
|
rename_request := fmt.tprintf("{{\"id\":10,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%s,\"line\":1,\"column\":5,\"newName\":\"renamedBroken\"}}}}\n", json_quote(diagnostics_path))
|
|
send_line(socket, rename_request)
|
|
read_messages(socket, 1)
|
|
}
|
|
|
|
run_editor_smoke :: proc(workspace: string) -> Editor {
|
|
editor := Editor{}
|
|
path := fmt.tprintf("%s/src/main/kotlin/dev/nativeeditor/daemon/Main.kt", workspace)
|
|
if !editor_open_file(&editor, path) {
|
|
return editor
|
|
}
|
|
|
|
fmt.println("editor-visible-before:")
|
|
editor_print_visible(&editor, 0, 2)
|
|
editor_replace_active_text(&editor, "fun broken( {")
|
|
fmt.println("editor-visible-after:")
|
|
editor_print_visible(&editor, 0, 2)
|
|
return editor
|
|
}
|
|
|
|
run_buffer_smoke :: proc() {
|
|
buffer := buffer_make("hello world\nsecond line\n")
|
|
defer buffer_destroy(&buffer)
|
|
|
|
buffer_insert(&buffer, 6, "native ")
|
|
buffer_delete_range(&buffer, 0, 6)
|
|
|
|
cursor := Cursor{}
|
|
cursor_move_to_line_col(&buffer, &cursor, 1, 6)
|
|
cursor_insert(&buffer, &cursor, " edited")
|
|
cursor_backspace(&buffer, &cursor)
|
|
_ = buffer_undo(&buffer, &cursor)
|
|
_ = buffer_redo(&buffer, &cursor)
|
|
cursor_move_vertical(&buffer, &cursor, -1)
|
|
|
|
line, col := buffer_offset_to_line_col(&buffer, cursor.offset)
|
|
second_line := buffer_line_bytes(&buffer, 1)
|
|
defer delete(second_line)
|
|
|
|
text := buffer_bytes(&buffer)
|
|
defer delete(text)
|
|
|
|
fmt.printf("buffer-smoke: %s", string(text[:]))
|
|
fmt.printf("buffer-lines: %d\n", buffer_line_count(&buffer))
|
|
fmt.printf("buffer-line-1: %s\n", string(second_line[:]))
|
|
fmt.printf("buffer-cursor: %d:%d\n", line, col)
|
|
}
|
|
|
|
send_line :: proc(socket: net.TCP_Socket, line: string) {
|
|
bytes := transmute([]byte)line
|
|
_, err := net.send_tcp(socket, bytes)
|
|
if err != nil {
|
|
fmt.println("send failed:", err)
|
|
}
|
|
}
|
|
|
|
read_messages :: proc(socket: net.TCP_Socket, expected: int) {
|
|
seen := 0
|
|
|
|
for seen < expected {
|
|
seen += read_some(socket)
|
|
}
|
|
}
|
|
|
|
read_some :: proc(socket: net.TCP_Socket) -> int {
|
|
buf: [4096]byte
|
|
n, err := net.recv_tcp(socket, buf[:])
|
|
if err != nil {
|
|
fmt.println("recv failed:", err)
|
|
return 0
|
|
}
|
|
if n == 0 {
|
|
fmt.println("connection closed")
|
|
return 0
|
|
}
|
|
|
|
lines := 0
|
|
for b in buf[:n] {
|
|
if b == '\n' {
|
|
lines += 1
|
|
}
|
|
}
|
|
|
|
fmt.print(string(buf[:n]))
|
|
return lines
|
|
}
|