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>
646 lines
19 KiB
Odin
646 lines
19 KiB
Odin
package main
|
|
|
|
import "core:fmt"
|
|
import "core:net"
|
|
import "core:os"
|
|
import "core:strconv"
|
|
import "core:strings"
|
|
import "core:sync"
|
|
import "core:thread"
|
|
import "core:time"
|
|
import json "core:encoding/json"
|
|
|
|
// Odin's %q verb emits \x/\e escapes that are not valid JSON, so outgoing
|
|
// strings are quoted here instead. Control characters become \u00XX.
|
|
json_quote :: proc(s: string, allocator := context.temp_allocator) -> string {
|
|
builder: strings.Builder
|
|
strings.builder_init(&builder, allocator)
|
|
strings.write_byte(&builder, '"')
|
|
for i := 0; i < len(s); i += 1 {
|
|
c := s[i]
|
|
switch c {
|
|
case '"':
|
|
strings.write_string(&builder, "\\\"")
|
|
case '\\':
|
|
strings.write_string(&builder, "\\\\")
|
|
case '\n':
|
|
strings.write_string(&builder, "\\n")
|
|
case '\r':
|
|
strings.write_string(&builder, "\\r")
|
|
case '\t':
|
|
strings.write_string(&builder, "\\t")
|
|
case:
|
|
if c < 0x20 {
|
|
fmt.sbprintf(&builder, "\\u%04x", c)
|
|
} else {
|
|
strings.write_byte(&builder, c)
|
|
}
|
|
}
|
|
}
|
|
strings.write_byte(&builder, '"')
|
|
return strings.to_string(builder)
|
|
}
|
|
|
|
Protocol_Message_Kind :: enum {
|
|
Invalid,
|
|
Response,
|
|
Event,
|
|
}
|
|
|
|
Protocol_Message :: struct {
|
|
kind: Protocol_Message_Kind,
|
|
id: int,
|
|
ok: bool,
|
|
event: string,
|
|
}
|
|
|
|
Daemon_Response :: struct {
|
|
id: int,
|
|
line: [dynamic]u8,
|
|
}
|
|
|
|
Daemon_Event :: struct {
|
|
line: [dynamic]u8,
|
|
}
|
|
|
|
Daemon_Process :: struct {
|
|
process: os.Process,
|
|
stdout: ^os.File,
|
|
running: bool,
|
|
}
|
|
|
|
Daemon_Client :: struct {
|
|
socket: net.TCP_Socket,
|
|
connected: bool,
|
|
next_id: int,
|
|
reader: ^thread.Thread,
|
|
event_mutex: sync.Mutex,
|
|
response_mutex: sync.Mutex,
|
|
diagnostic_events: [dynamic]Daemon_Event,
|
|
gradle_events: [dynamic]Daemon_Event,
|
|
pending_response_ids: [dynamic]int,
|
|
responses: [dynamic]Daemon_Response,
|
|
}
|
|
|
|
daemon_connect :: proc(port: int) -> Daemon_Client {
|
|
client := Daemon_Client{next_id = 1}
|
|
if port <= 0 do return client
|
|
|
|
socket, err := net.dial_tcp("127.0.0.1", port)
|
|
if err != nil {
|
|
fmt.println("daemon connect failed:", err)
|
|
return client
|
|
}
|
|
|
|
client.socket = socket
|
|
client.connected = true
|
|
return client
|
|
}
|
|
|
|
daemon_start_process :: proc(workspace: string) -> (Daemon_Process, int, bool) {
|
|
child, ok := daemon_start_process_begin(workspace)
|
|
if !ok {
|
|
return child, 0, false
|
|
}
|
|
|
|
port, port_ok := daemon_read_port(&child)
|
|
if !port_ok {
|
|
daemon_stop_process(&child)
|
|
return child, 0, false
|
|
}
|
|
return child, port, true
|
|
}
|
|
|
|
daemon_start_process_begin :: proc(workspace: string) -> (Daemon_Process, bool) {
|
|
child := Daemon_Process{}
|
|
|
|
stdout_r, stdout_w, pipe_err := os.pipe()
|
|
if pipe_err != nil {
|
|
fmt.println("daemon pipe failed:", pipe_err)
|
|
return child, false
|
|
}
|
|
|
|
command := []string{"gradle", "-p", workspace, "run", "--quiet"}
|
|
process, start_err := os.process_start(os.Process_Desc{
|
|
command = command,
|
|
stdout = stdout_w,
|
|
})
|
|
_ = os.close(stdout_w)
|
|
|
|
if start_err != nil {
|
|
fmt.println("daemon start failed:", start_err)
|
|
_ = os.close(stdout_r)
|
|
return child, false
|
|
}
|
|
|
|
child = Daemon_Process{process = process, stdout = stdout_r, running = true}
|
|
return child, true
|
|
}
|
|
|
|
daemon_stop_process :: proc(child: ^Daemon_Process) {
|
|
if child.stdout != nil {
|
|
_ = os.close(child.stdout)
|
|
child.stdout = nil
|
|
}
|
|
if child.running {
|
|
_ = os.process_terminate(child.process)
|
|
state, wait_err := os.process_wait(child.process, 2 * time.Second)
|
|
if wait_err != nil || !state.exited {
|
|
_ = os.process_kill(child.process)
|
|
_, _ = os.process_wait(child.process)
|
|
}
|
|
child.running = false
|
|
}
|
|
}
|
|
|
|
daemon_read_port :: proc(child: ^Daemon_Process) -> (int, bool) {
|
|
line: [dynamic]u8
|
|
defer delete(line)
|
|
|
|
start := time.now()
|
|
buf: [1024]u8
|
|
for time.diff(start, time.now()) < 30 * time.Second {
|
|
state, wait_err := os.process_wait(child.process, 0)
|
|
if wait_err == nil && state.exited {
|
|
fmt.println("daemon exited before announcing port")
|
|
return 0, false
|
|
}
|
|
|
|
has_data, data_err := os.pipe_has_data(child.stdout)
|
|
if data_err != nil {
|
|
fmt.println("daemon stdout failed:", data_err)
|
|
return 0, false
|
|
}
|
|
if !has_data {
|
|
time.sleep(100 * time.Millisecond)
|
|
continue
|
|
}
|
|
|
|
n, read_err := os.read(child.stdout, buf[:])
|
|
if read_err != nil {
|
|
fmt.println("daemon stdout read failed:", read_err)
|
|
return 0, false
|
|
}
|
|
|
|
for b in buf[:n] {
|
|
if b == '\n' {
|
|
port, ok := daemon_parse_port_line(string(line[:]))
|
|
if ok do return port, true
|
|
clear(&line)
|
|
} else {
|
|
append(&line, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.println("daemon start timed out")
|
|
return 0, false
|
|
}
|
|
|
|
daemon_poll_port :: proc(child: ^Daemon_Process, line: ^[dynamic]u8) -> (int, bool, bool) {
|
|
if child == nil || !child.running || child.stdout == nil do return 0, false, true
|
|
|
|
state, wait_err := os.process_wait(child.process, 0)
|
|
if wait_err == nil && state.exited {
|
|
fmt.println("daemon exited before announcing port")
|
|
return 0, false, true
|
|
}
|
|
|
|
has_data, data_err := os.pipe_has_data(child.stdout)
|
|
if data_err != nil {
|
|
fmt.println("daemon stdout failed:", data_err)
|
|
return 0, false, true
|
|
}
|
|
if !has_data do return 0, false, false
|
|
|
|
buf: [1024]u8
|
|
n, read_err := os.read(child.stdout, buf[:])
|
|
if read_err != nil {
|
|
fmt.println("daemon stdout read failed:", read_err)
|
|
return 0, false, true
|
|
}
|
|
|
|
for b in buf[:n] {
|
|
if b == '\n' {
|
|
port, ok := daemon_parse_port_line(string(line[:]))
|
|
clear(line)
|
|
if ok do return port, true, false
|
|
} else {
|
|
append(line, b)
|
|
}
|
|
}
|
|
|
|
return 0, false, false
|
|
}
|
|
|
|
daemon_parse_port_line :: proc(line: string) -> (int, bool) {
|
|
if !strings.has_prefix(line, "PORT ") do return 0, false
|
|
port, ok := strconv.parse_int(line[len("PORT "):])
|
|
if !ok do return 0, false
|
|
return int(port), true
|
|
}
|
|
|
|
daemon_start_reader :: proc(client: ^Daemon_Client) {
|
|
if client.connected && client.reader == nil {
|
|
client.reader = thread.create_and_start_with_data(rawptr(client), daemon_reader_thread)
|
|
}
|
|
}
|
|
|
|
daemon_close :: proc(client: ^Daemon_Client) {
|
|
if client.connected {
|
|
net.close(client.socket)
|
|
client.connected = false
|
|
}
|
|
if client.reader != nil && thread.is_done(client.reader) {
|
|
thread.destroy(client.reader)
|
|
client.reader = nil
|
|
}
|
|
for &event in client.diagnostic_events {
|
|
delete(event.line)
|
|
}
|
|
delete(client.diagnostic_events)
|
|
client.diagnostic_events = nil
|
|
for &event in client.gradle_events {
|
|
delete(event.line)
|
|
}
|
|
delete(client.gradle_events)
|
|
client.gradle_events = nil
|
|
delete(client.pending_response_ids)
|
|
client.pending_response_ids = nil
|
|
for &response in client.responses {
|
|
delete(response.line)
|
|
}
|
|
delete(client.responses)
|
|
client.responses = nil
|
|
}
|
|
|
|
daemon_send :: proc(client: ^Daemon_Client, message: string) {
|
|
if !client.connected do return
|
|
bytes := transmute([]byte)message
|
|
_, err := net.send_tcp(client.socket, bytes)
|
|
if err != nil {
|
|
fmt.println("daemon send failed:", err)
|
|
daemon_close(client)
|
|
}
|
|
}
|
|
|
|
daemon_track_response :: proc(client: ^Daemon_Client, id: int) {
|
|
if id == 0 do return
|
|
if sync.mutex_guard(&client.response_mutex) {
|
|
append(&client.pending_response_ids, id)
|
|
}
|
|
}
|
|
|
|
daemon_send_workspace_open :: proc(client: ^Daemon_Client, workspace: string) {
|
|
if !client.connected do return
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"workspace/open\",\"params\":{{\"root\":%s}}}}\n", id, json_quote(workspace))
|
|
daemon_send(client, request)
|
|
}
|
|
|
|
daemon_send_text_open :: proc(client: ^Daemon_Client, path: string, version: int, text: string) {
|
|
if !client.connected do return
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/open\",\"params\":{{\"path\":%s,\"version\":%d,\"text\":%s}}}}\n", id, json_quote(path), version, json_quote(text))
|
|
daemon_send(client, request)
|
|
}
|
|
|
|
daemon_send_text_change :: proc(client: ^Daemon_Client, path: string, version: int, text: string) {
|
|
if !client.connected do return
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"text/change\",\"params\":{{\"path\":%s,\"version\":%d,\"text\":%s}}}}\n", id, json_quote(path), version, json_quote(text))
|
|
daemon_send(client, request)
|
|
}
|
|
|
|
daemon_send_completion :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/completion\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_send_hover :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/hover\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_send_definition :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/definition\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_send_references :: proc(client: ^Daemon_Client, path: string, line, column: int) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/references\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d}}}}\n", id, json_quote(path), line + 1, column + 1)
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_send_rename :: proc(client: ^Daemon_Client, path: string, line, column: int, new_name: string) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"kotlin/rename\",\"params\":{{\"path\":%s,\"line\":%d,\"column\":%d,\"newName\":%s}}}}\n", id, json_quote(path), line + 1, column + 1, json_quote(new_name))
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_send_gradle_tasks :: proc(client: ^Daemon_Client) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"gradle/tasks\",\"params\":{{}}}}\n", id)
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_send_gradle_run :: proc(client: ^Daemon_Client, task: string) -> int {
|
|
if !client.connected do return 0
|
|
id := client.next_id
|
|
client.next_id += 1
|
|
request := fmt.tprintf("{{\"id\":%d,\"method\":\"gradle/run\",\"params\":{{\"task\":%s}}}}\n", id, json_quote(task))
|
|
daemon_track_response(client, id)
|
|
daemon_send(client, request)
|
|
return id
|
|
}
|
|
|
|
daemon_sync_active_buffer :: proc(client: ^Daemon_Client, editor: ^Editor, open: bool) {
|
|
active := editor_active_buffer(editor)
|
|
if active == nil do return
|
|
|
|
text := editor_active_text(editor)
|
|
defer delete(text)
|
|
|
|
if open {
|
|
daemon_send_text_open(client, active.path, active.buffer.version, string(text[:]))
|
|
} else {
|
|
daemon_send_text_change(client, active.path, active.buffer.version, string(text[:]))
|
|
}
|
|
}
|
|
|
|
daemon_reader_thread :: proc(data: rawptr) {
|
|
client := (^Daemon_Client)(data)
|
|
buf: [4096]u8
|
|
line: [dynamic]u8
|
|
defer delete(line)
|
|
|
|
for client.connected {
|
|
n, err := net.recv_tcp(client.socket, buf[:])
|
|
if err != nil || n == 0 {
|
|
client.connected = false
|
|
break
|
|
}
|
|
|
|
for b in buf[:n] {
|
|
if b == '\n' {
|
|
daemon_store_line(client, string(line[:]))
|
|
clear(&line)
|
|
} else {
|
|
append(&line, b)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
daemon_store_line :: proc(client: ^Daemon_Client, line: string) {
|
|
message, value, ok := parse_protocol_message(line)
|
|
if !ok do return
|
|
defer json.destroy_value(value)
|
|
if message.kind == .Response {
|
|
if sync.mutex_guard(&client.response_mutex) {
|
|
tracked := false
|
|
for pending_id, index in client.pending_response_ids {
|
|
if pending_id == message.id {
|
|
ordered_remove(&client.pending_response_ids, index)
|
|
tracked = true
|
|
break
|
|
}
|
|
}
|
|
if !tracked do return
|
|
|
|
response := Daemon_Response{id = message.id}
|
|
for b in transmute([]u8)line {
|
|
append(&response.line, b)
|
|
}
|
|
append(&client.responses, response)
|
|
}
|
|
return
|
|
}
|
|
if message.kind != .Event do return
|
|
|
|
if message.event == "gradle/run" {
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
event := Daemon_Event{}
|
|
for b in transmute([]u8)line {
|
|
append(&event.line, b)
|
|
}
|
|
append(&client.gradle_events, event)
|
|
}
|
|
return
|
|
}
|
|
|
|
if message.event != "diagnostics/publish" do return
|
|
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
event := Daemon_Event{}
|
|
for b in transmute([]u8)line {
|
|
append(&event.line, b)
|
|
}
|
|
append(&client.diagnostic_events, event)
|
|
}
|
|
}
|
|
|
|
daemon_take_response :: proc(client: ^Daemon_Client, expected_id: int) -> [dynamic]u8 {
|
|
out: [dynamic]u8
|
|
if expected_id == 0 do return out
|
|
|
|
if sync.mutex_guard(&client.response_mutex) {
|
|
for &response, index in client.responses {
|
|
if response.id != expected_id do continue
|
|
for b in response.line {
|
|
append(&out, b)
|
|
}
|
|
delete(response.line)
|
|
ordered_remove(&client.responses, index)
|
|
break
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
daemon_take_diagnostic_event :: proc(client: ^Daemon_Client) -> [dynamic]u8 {
|
|
out: [dynamic]u8
|
|
if !client.connected && len(client.diagnostic_events) == 0 do return out
|
|
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
if len(client.diagnostic_events) == 0 do return out
|
|
event := client.diagnostic_events[0]
|
|
for b in event.line {
|
|
append(&out, b)
|
|
}
|
|
delete(event.line)
|
|
ordered_remove(&client.diagnostic_events, 0)
|
|
}
|
|
return out
|
|
}
|
|
|
|
daemon_take_gradle_event :: proc(client: ^Daemon_Client) -> [dynamic]u8 {
|
|
out: [dynamic]u8
|
|
if !client.connected && len(client.gradle_events) == 0 do return out
|
|
|
|
if sync.mutex_guard(&client.event_mutex) {
|
|
if len(client.gradle_events) == 0 do return out
|
|
event := client.gradle_events[0]
|
|
for b in event.line {
|
|
append(&out, b)
|
|
}
|
|
delete(event.line)
|
|
ordered_remove(&client.gradle_events, 0)
|
|
}
|
|
return out
|
|
}
|
|
|
|
daemon_apply_latest_diagnostics :: proc(client: ^Daemon_Client, editor: ^Editor) {
|
|
for {
|
|
event := daemon_take_diagnostic_event(client)
|
|
if len(event) == 0 {
|
|
delete(event)
|
|
return
|
|
}
|
|
|
|
path, diagnostics := parse_diagnostics_event(string(event[:]))
|
|
if len(path) == 0 || !editor_set_diagnostics_for_path(editor, path, diagnostics[:]) {
|
|
diagnostics_destroy(diagnostics[:])
|
|
}
|
|
delete(path)
|
|
delete(diagnostics)
|
|
delete(event)
|
|
}
|
|
}
|
|
|
|
diagnostics_destroy :: proc(diagnostics: []Diagnostic) {
|
|
for diagnostic in diagnostics {
|
|
delete(diagnostic.severity)
|
|
delete(diagnostic.message)
|
|
}
|
|
}
|
|
|
|
parse_diagnostics_event :: proc(event: string) -> (string, [dynamic]Diagnostic) {
|
|
diagnostics: [dynamic]Diagnostic
|
|
|
|
message, value, parsed := parse_protocol_message(event)
|
|
if !parsed do return "", diagnostics
|
|
defer json.destroy_value(value)
|
|
if message.kind != .Event || message.event != "diagnostics/publish" do return "", diagnostics
|
|
|
|
params, has_params := json_object_get(value, "params")
|
|
if !has_params do return "", diagnostics
|
|
path, _ := json_get_string(params, "path")
|
|
diagnostics_value, has_diagnostics := json_object_get(params, "diagnostics")
|
|
if !has_diagnostics do return strings.clone(path), diagnostics
|
|
|
|
#partial switch items in diagnostics_value {
|
|
case json.Array:
|
|
for item in items {
|
|
severity, _ := json_get_string(item, "severity")
|
|
diagnostic_message, _ := json_get_string(item, "message")
|
|
line, _ := json_get_int(item, "line")
|
|
column, _ := json_get_int(item, "column")
|
|
|
|
append(&diagnostics, Diagnostic{
|
|
line = max_int(line - 1, 0),
|
|
column = max_int(column - 1, 0),
|
|
severity = strings.clone(severity),
|
|
message = strings.clone(diagnostic_message),
|
|
})
|
|
}
|
|
}
|
|
|
|
return strings.clone(path), diagnostics
|
|
}
|
|
|
|
parse_protocol_message :: proc(line: string) -> (Protocol_Message, json.Value, bool) {
|
|
value, err := json.parse_string(line, .JSON, true)
|
|
if err != nil {
|
|
return Protocol_Message{}, value, false
|
|
}
|
|
|
|
message := Protocol_Message{}
|
|
if event, ok := json_get_string(value, "event"); ok {
|
|
message.kind = .Event
|
|
message.event = event
|
|
return message, value, true
|
|
}
|
|
|
|
if id, ok := json_get_int(value, "id"); ok {
|
|
message.kind = .Response
|
|
message.id = id
|
|
message.ok, _ = json_get_bool(value, "ok")
|
|
return message, value, true
|
|
}
|
|
|
|
return message, value, false
|
|
}
|
|
|
|
json_object_get :: proc(value: json.Value, key: string) -> (json.Value, bool) {
|
|
#partial switch object in value {
|
|
case json.Object:
|
|
if item, ok := object[key]; ok {
|
|
return item, true
|
|
}
|
|
}
|
|
return json.Value{}, false
|
|
}
|
|
|
|
json_get_string :: proc(value: json.Value, key: string) -> (string, bool) {
|
|
item, ok := json_object_get(value, key)
|
|
if !ok do return "", false
|
|
#partial switch s in item {
|
|
case json.String:
|
|
return string(s), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
json_get_int :: proc(value: json.Value, key: string) -> (int, bool) {
|
|
item, ok := json_object_get(value, key)
|
|
if !ok do return 0, false
|
|
#partial switch n in item {
|
|
case json.Integer:
|
|
return int(n), true
|
|
case json.Float:
|
|
return int(n), true
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
json_get_bool :: proc(value: json.Value, key: string) -> (bool, bool) {
|
|
item, ok := json_object_get(value, key)
|
|
if !ok do return false, false
|
|
#partial switch b in item {
|
|
case json.Boolean:
|
|
return bool(b), true
|
|
}
|
|
return false, false
|
|
}
|