diff --git a/tests/Makefile b/tests/Makefile index 0d9c173..2389e4b 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,27 +1,35 @@ # SPDX-License-Identifier: GPL-2.0-only CC ?= gcc -CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd +CFLAGS ?= -O2 -g -Wall -Wextra -Wno-unused-parameter -Wno-sign-compare -std=gnu11 -D_GNU_SOURCE +INCLUDES := -I../tools/opengc570d-userd/src -I../tools/opengc570d-userd -I../tools/opengc573-userd DEFS := -DGC570D_USERSPACE -DRIVER_DIR := ../tools/opengc570d-userd/src +GC570D_DIR := ../tools/opengc570d-userd/src +GC573_DIR := ../tools/opengc573-userd -TARGETS := test_edid test_config +TARGETS := test_edid test_config test_ipc_json all: $(TARGETS) -test_edid: test_edid.c $(DRIVER_DIR)/gc570d-receiver.c $(DRIVER_DIR)/gc570d-splitter.c +test_edid: test_edid.c $(GC570D_DIR)/gc570d-receiver.c $(GC570D_DIR)/gc570d-splitter.c $(CC) $(CFLAGS) $(INCLUDES) $(DEFS) -o $@ $^ -test_config: test_config.c $(DRIVER_DIR)/gc570d-config.c +test_config: test_config.c $(GC570D_DIR)/gc570d-config.c $(CC) $(CFLAGS) $(INCLUDES) $(DEFS) -o $@ $^ +test_ipc_json: test_ipc_json.c $(GC573_DIR)/third_party/jsmn.c + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $^ -pthread + +asan: CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer +asan: all + test: all - @echo "Running GC570D unit tests..." + @echo "Running GC570D & GC573 unit tests..." @./test_edid @./test_config + @./test_ipc_json clean: rm -f $(TARGETS) -.PHONY: all test clean +.PHONY: all test clean asan diff --git a/tests/test_ipc_json.c b/tests/test_ipc_json.c new file mode 100644 index 0000000..fdcf9e9 --- /dev/null +++ b/tests/test_ipc_json.c @@ -0,0 +1,245 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_ipc_json.c — Unit tests for GC573 Hand-rolled JSON Scanner and RPC Formatter + */ + +#include +#include +#include +#include +#include + +#include "../tools/opengc573-userd/gc573-user-compat.h" +#include "../tools/opengc573-userd/gc573_user_ipc.c" + +static int g_failures = 0; + +#define TEST_ASSERT(cond, msg) do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s (line %d)\n", msg, __LINE__); \ + g_failures++; \ + } else { \ + printf(" [PASS] %s\n", msg); \ + } \ +} while (0) + +static void test_json_get_string(void) +{ + char out[64]; + const char *json = "{\"jsonrpc\":\"2.0\",\"method\":\"led.set\",\"params\":{\"mode\":\"wave\"}}"; + const char *json_dup_val = "{\"title\":\"mode\",\"mode\":\"wave\"}"; + const char *json_param_method = "{\"jsonrpc\":\"2.0\",\"params\":{\"text\":\"method\"},\"method\":\"led.set\"}"; + + printf("Testing json_get_string...\n"); + TEST_ASSERT(json_get_string(json, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, + "Extracts top-level string 'method'"); + TEST_ASSERT(json_get_string(json, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, + "Extracts nested string 'mode'"); + TEST_ASSERT(json_get_string(json_dup_val, "mode", out, sizeof(out)) && strcmp(out, "wave") == 0, + "Distinguishes object key from prior string value matching key name"); + TEST_ASSERT(json_get_string(json_param_method, "method", out, sizeof(out)) && strcmp(out, "led.set") == 0, + "Extracts top-level method even if params contains identical string value"); + TEST_ASSERT(!json_get_string(json, "missing", out, sizeof(out)), + "Fails cleanly on missing key"); +} + +static void test_json_get_long(void) +{ + long val = 0; + const char *json = "{\"id\":42,\"code\":-32700,\"zero\":0}"; + + printf("Testing json_get_long...\n"); + TEST_ASSERT(json_get_long(json, "id", &val) && val == 42, + "Extracts positive integer 'id': 42"); + TEST_ASSERT(json_get_long(json, "code", &val) && val == -32700, + "Extracts negative integer 'code': -32700"); + TEST_ASSERT(json_get_long(json, "zero", &val) && val == 0, + "Extracts zero integer: 0"); + TEST_ASSERT(!json_get_long(json, "nonexistent", &val), + "Fails cleanly on non-existent key"); +} + +static void test_json_get_bool(void) +{ + bool val = false; + const char *json = "{\"active\":true,\"disabled\":false,\"num\":1}"; + + printf("Testing json_get_bool...\n"); + TEST_ASSERT(json_get_bool(json, "active", &val) && val == true, + "Extracts boolean true"); + TEST_ASSERT(json_get_bool(json, "disabled", &val) && val == false, + "Extracts boolean false"); + TEST_ASSERT(!json_get_bool(json, "num", &val), + "Rejects non-boolean value"); +} + +static void test_json_get_double(void) +{ + double val = 0.0; + const char *json = "{\"exposure\":-1.5,\"gain\":0.85}"; + + printf("Testing json_get_double...\n"); + TEST_ASSERT(json_get_double(json, "exposure", &val) && val == -1.5, + "Extracts negative float 'exposure': -1.5"); + TEST_ASSERT(json_get_double(json, "gain", &val) && val > 0.84 && val < 0.86, + "Extracts positive float 'gain': 0.85"); + TEST_ASSERT(!json_get_double(json, "nonexistent", &val), + "Fails cleanly on missing float key"); +} + +static void test_json_escape_into(void) +{ + char dst[128]; + + printf("Testing json_escape_into...\n"); + json_escape_into(dst, sizeof(dst), "Simple string"); + TEST_ASSERT(strcmp(dst, "Simple string") == 0, "Leaves plain ASCII unchanged"); + + json_escape_into(dst, sizeof(dst), "Hello \"World\"\\Test\nNew\tTab"); + TEST_ASSERT(strcmp(dst, "Hello \\\"World\\\"\\\\Test\\u000aNew\\u0009Tab") == 0, + "Escapes quotes, backslashes, and control characters"); +} + +static void test_json_format_id(void) +{ + char dst[64]; + + printf("Testing json_format_id...\n"); + json_format_id(dst, sizeof(dst), NULL); + TEST_ASSERT(strcmp(dst, "null") == 0, "Formats NULL id as null"); + + json_format_id(dst, sizeof(dst), "null"); + TEST_ASSERT(strcmp(dst, "null") == 0, "Formats 'null' string as null"); + + json_format_id(dst, sizeof(dst), "1234"); + TEST_ASSERT(strcmp(dst, "1234") == 0, "Preserves numeric integer id 1234"); + + json_format_id(dst, sizeof(dst), "-1"); + TEST_ASSERT(strcmp(dst, "-1") == 0, "Preserves negative integer id -1"); + + json_format_id(dst, sizeof(dst), "\"client-req-99\""); + TEST_ASSERT(strcmp(dst, "\"client-req-99\"") == 0, "Preserves and sanitizes quoted string id"); + + json_format_id(dst, sizeof(dst), "raw-id"); + TEST_ASSERT(strcmp(dst, "\"raw-id\"") == 0, "Encapsulates unquoted string id"); + + json_format_id(dst, sizeof(dst), "\"injection\n\""); + TEST_ASSERT(strstr(dst, "\\u000a") != NULL, "Re-escapes dangerous newline injection in string id"); +} + +static void test_json_find_value(void) +{ + char out[64]; + const char *json_num = "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"ping\"}"; + const char *json_str = "{\"jsonrpc\":\"2.0\",\"id\":\"req-abc\",\"method\":\"status.get\"}"; + const char *json_null = "{\"jsonrpc\":\"2.0\",\"id\":null,\"method\":\"led.set\"}"; + + printf("Testing json_find_value...\n"); + TEST_ASSERT(json_find_value(json_num, "method", out, sizeof(out)) && strcmp(out, "ping") == 0, + "Extracts method string from numeric ID request"); + TEST_ASSERT(json_find_value(json_num, "id", out, sizeof(out)) && strcmp(out, "42") == 0, + "Extracts numeric ID: 42"); + + TEST_ASSERT(json_find_value(json_str, "method", out, sizeof(out)) && strcmp(out, "status.get") == 0, + "Extracts method string from string ID request"); + TEST_ASSERT(json_find_value(json_str, "id", out, sizeof(out)) && strcmp(out, "req-abc") == 0, + "Extracts string ID: req-abc"); + + TEST_ASSERT(json_find_value(json_null, "id", out, sizeof(out)) && strcmp(out, "null") == 0, + "Extracts null ID: null"); + TEST_ASSERT(!json_find_value(json_num, "missing", out, sizeof(out)), + "Fails cleanly on missing key in json_find_value"); +} + +static void test_json_negative_edge_cases(void) +{ + char out[64]; + long lval = 0; + double dval = 0.0; + bool bval = false; + const char *valid_json = "{\"str\":\"hello\",\"num\":123,\"flag\":true,\"flt\":3.14}"; + + printf("Testing JSON negative edge cases...\n"); + + /* NULL pointer handling */ + TEST_ASSERT(!json_get_string(NULL, "str", out, sizeof(out)), "json_get_string rejects NULL json"); + TEST_ASSERT(!json_get_string(valid_json, NULL, out, sizeof(out)), "json_get_string rejects NULL key"); + TEST_ASSERT(!json_get_string(valid_json, "str", NULL, sizeof(out)), "json_get_string rejects NULL output"); + TEST_ASSERT(!json_get_string(valid_json, "str", out, 0), "json_get_string rejects 0 outsz"); + + TEST_ASSERT(!json_get_long(NULL, "num", &lval), "json_get_long rejects NULL json"); + TEST_ASSERT(!json_get_long(valid_json, NULL, &lval), "json_get_long rejects NULL key"); + TEST_ASSERT(!json_get_long(valid_json, "num", NULL), "json_get_long rejects NULL output"); + + TEST_ASSERT(!json_get_double(NULL, "flt", &dval), "json_get_double rejects NULL json"); + TEST_ASSERT(!json_get_double(valid_json, NULL, &dval), "json_get_double rejects NULL key"); + TEST_ASSERT(!json_get_double(valid_json, "flt", NULL), "json_get_double rejects NULL output"); + + TEST_ASSERT(!json_get_bool(NULL, "flag", &bval), "json_get_bool rejects NULL json"); + TEST_ASSERT(!json_get_bool(valid_json, NULL, &bval), "json_get_bool rejects NULL key"); + TEST_ASSERT(!json_get_bool(valid_json, "flag", NULL), "json_get_bool rejects NULL output"); + + TEST_ASSERT(!json_find_value(NULL, "id", out, sizeof(out)), "json_find_value rejects NULL json"); + TEST_ASSERT(!json_find_value(valid_json, NULL, out, sizeof(out)), "json_find_value rejects NULL key"); + TEST_ASSERT(!json_find_value(valid_json, "id", NULL, sizeof(out)), "json_find_value rejects NULL output"); + TEST_ASSERT(!json_find_value(valid_json, "id", out, 0), "json_find_value rejects 0 outsz"); + + /* Malformed JSON input */ + TEST_ASSERT(!json_get_string("", "str", out, sizeof(out)), "Rejects empty JSON string"); + TEST_ASSERT(!json_get_string("not json at all", "str", out, sizeof(out)), "Rejects non-JSON string"); + TEST_ASSERT(!json_get_string("{\"unclosed\":", "unclosed", out, sizeof(out)), "Rejects unclosed JSON"); + TEST_ASSERT(!json_get_string("[1, 2, 3]", "str", out, sizeof(out)), "Rejects array root JSON"); + TEST_ASSERT(!json_get_string("{unquoted_key: 1}", "unquoted_key", out, sizeof(out)), "Rejects unquoted keys"); + + /* Type mismatches */ + TEST_ASSERT(!json_get_string(valid_json, "num", out, sizeof(out)), + "Type mismatch: json_get_string rejects primitive number"); + TEST_ASSERT(!json_get_long(valid_json, "str", &lval), + "Type mismatch: json_get_long rejects string value"); + TEST_ASSERT(!json_get_double(valid_json, "str", &dval), + "Type mismatch: json_get_double rejects string value"); + TEST_ASSERT(!json_get_bool(valid_json, "str", &bval), + "Type mismatch: json_get_bool rejects string value"); + TEST_ASSERT(!json_get_bool(valid_json, "num", &bval), + "Type mismatch: json_get_bool rejects number value"); + + /* Destination buffer too small for string */ + char small_buf[4]; + TEST_ASSERT(!json_get_string(valid_json, "str", small_buf, sizeof(small_buf)), + "Safely rejects string when destination buffer is too small"); + + /* json_escape_into negative edge cases */ + json_escape_into(NULL, 10, "test"); /* Should not crash */ + json_escape_into(out, 0, "test"); /* Should not crash */ + json_escape_into(out, sizeof(out), NULL); + TEST_ASSERT(out[0] == '\0', "json_escape_into handles NULL src cleanly"); + + /* json_format_id negative edge cases */ + json_format_id(NULL, 10, "id"); /* Should not crash */ + json_format_id(out, 0, "id"); /* Should not crash */ +} + +int main(void) +{ + printf("==================================================\n"); + printf(" GC573 IPC JSON Scanner Test Suite\n"); + printf("==================================================\n"); + + test_json_get_string(); + test_json_get_long(); + test_json_get_double(); + test_json_get_bool(); + test_json_escape_into(); + test_json_format_id(); + test_json_find_value(); + test_json_negative_edge_cases(); + + printf("==================================================\n"); + if (g_failures == 0) { + printf(" ALL GC573 IPC JSON TESTS PASSED!\n"); + return 0; + } else { + printf(" %d TESTS FAILED!\n", g_failures); + return 1; + } +} diff --git a/tools/opengc573-userd/Makefile b/tools/opengc573-userd/Makefile index 3ff88dd..76a1ba5 100644 --- a/tools/opengc573-userd/Makefile +++ b/tools/opengc573-userd/Makefile @@ -17,7 +17,7 @@ PW_LIBS = $(shell pkg-config --libs libpipewire-0.3) # Frame generation is delegated to the system lsfg-vk Vulkan implicit layer, # which the SDL2 "vulkan" renderer feeds directly. No GL, no Vulkan headers. -COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_it6664.o +COMMON_OBJS = gc573_vfio.o gc573_user_i2c.o gc573_user_hw.o gc573_user_edid.o gc573_user_it6805.o gc573_user_it6664.o third_party/jsmn.o STREAM_OBJS = gc573_user_fpga.o gc573_user_video.o gc573_user_audio.o gc573_user_bitstream.o gc573_user_nosignal.o gc573_user_ipc.o gc573_user_audio_tap.o gc573_user_hdr.o gc573_user_framegen.o TARGETS = gc573_user_probe gc573_user_streamer everlive_userd gc573_user_diag HEADERS = gc573-user-compat.h gc573_vfio.h gc573_user_i2c.h gc573_user_hw.h gc573_user_edid.h gc573_user_it6805.h gc573_user_it6664.h gc573_user_fpga.h gc573_user_video.h gc573_user_audio.h gc573_user_bitstream.h gc573_user_nosignal.h gc573_user_ipc.h gc573_user_audio_tap.h gc573_user_hdr.h gc573_user_framegen.h @@ -43,6 +43,9 @@ gc573_user_diag: gc573_user_diag.o $(COMMON_OBJS) gc573_framegen_selftest: test_framegen.o gc573_user_framegen.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -lm +third_party/jsmn.o: third_party/jsmn.c third_party/jsmn.h + $(CC) $(CFLAGS) -c -o $@ $< + selftest: gc573_framegen_selftest ./gc573_framegen_selftest diff --git a/tools/opengc573-userd/gc573_user_ipc.c b/tools/opengc573-userd/gc573_user_ipc.c index aa73184..85ab000 100644 --- a/tools/opengc573-userd/gc573_user_ipc.c +++ b/tools/opengc573-userd/gc573_user_ipc.c @@ -25,6 +25,11 @@ #include #include +/* jsmn JSON parser - single header library (MIT licensed) */ +#define JSMN_HEADER +#define JSMN_PARENT_LINKS +#include "third_party/jsmn.h" + /* * Control socket name shared by the engine and the Console Hub. */ @@ -117,200 +122,221 @@ static uint64_t ipc_now_ms(void) return (uint64_t)ts.tv_sec * 1000U + (uint64_t)(ts.tv_nsec / 1000000L); } -static const char *skip_ws(const char *p) -{ - while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') - p++; - return p; -} - -/* - * Copy a JSON value token from @p into @out. Strings keep their quotes, so - * the result can be embedded in a reply unchanged; numbers, booleans and null - * are copied up to the next structural character. - */ -static bool json_copy_token(const char *p, char *out, size_t outsz) +/* JSON parsing with jsmn - limits: 128 tokens, max depth 16, max input 16KB */ +#define IPC_JSON_MAX_TOKENS 128 +#define IPC_JSON_MAX_DEPTH 16 +#define IPC_JSON_MAX_INPUT (16 * 1024) + +typedef enum { + JSON_GET_STRING, + JSON_GET_LONG, + JSON_GET_DOUBLE, + JSON_GET_BOOL, + JSON_GET_RAW +} json_get_type_e; + +/* Find a key in the JSON token tree. Direct children of root are matched first. */ +static int json_token_find_key(const jsmntok_t *tokens, int tok_count, + const char *key, const char *json) { - const char *start = p; - const char *end; + const size_t klen = strlen(key); - if (*p == '"') { - bool esc = false; + /* Pass 1: check direct keys of the root object (tokens[0]) */ + for (int i = 1; i < tok_count; i++) { + if (tokens[i].type != JSMN_STRING || tokens[i].parent != 0) + continue; + size_t len = (size_t)(tokens[i].end - tokens[i].start); + if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { + if (i + 1 < tok_count) + return i + 1; + } + } - p++; - while (*p) { - if (esc) { - esc = false; - p++; - continue; - } - if (*p == '\\') { - esc = true; - p++; - continue; - } - if (*p == '"') { - p++; - break; - } - p++; + /* Pass 2: check keys of nested objects (e.g. inside "params") */ + for (int i = 1; i < tok_count; i++) { + if (tokens[i].type != JSMN_STRING) + continue; + if (tokens[i].parent <= 0 || tokens[tokens[i].parent].type != JSMN_OBJECT) + continue; + size_t len = (size_t)(tokens[i].end - tokens[i].start); + if (len == klen && memcmp(json + tokens[i].start, key, klen) == 0) { + if (i + 1 < tok_count) + return i + 1; } - end = p; - } else { - while (*p && *p != ',' && *p != '}' && *p != ']' && - *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n') - p++; - end = p; } - size_t len = (size_t)(end - start); + return -1; +} - if (len == 0 || len >= outsz) +static bool json_token_to_string(const jsmntok_t *tok, const char *json, + char *out, size_t outsz) +{ + if (!tok || tok->type != JSMN_STRING) + return false; + int len = tok->end - tok->start; + if (len < 0 || (size_t)len >= outsz) return false; - memcpy(out, start, len); + memcpy(out, json + tok->start, len); out[len] = '\0'; return true; } -/* - * Find "key": in a flat JSON object and copy the raw value token. - * Key names are matched only in key position, so a string value containing the - * key name is never mistaken for the key itself. - */ -static bool json_find_value(const char *s, const char *key, char *out, - size_t outsz) +static bool json_token_to_long(const jsmntok_t *tok, const char *json, + long *out) { - const size_t klen = strlen(key); - const char *p = s; - - while (*p) { - const char *start; - const char *end; - bool esc = false; - - if (*p != '"') { - if (*p == '\\' && p[1]) { - p += 2; - continue; - } - p++; - continue; - } - - start = p + 1; - end = start; - while (*end) { - if (esc) { - esc = false; - end++; - continue; - } - if (*end == '\\') { - esc = true; - end++; - continue; - } - if (*end == '"') - break; - end++; - } - if (!*end) - return false; /* unterminated string */ + if (!tok || tok->type != JSMN_PRIMITIVE) + return false; + char buf[64]; + int len = tok->end - tok->start; + if (len < 0 || len >= (int)sizeof(buf)) + return false; + memcpy(buf, json + tok->start, len); + buf[len] = '\0'; + char *end; + errno = 0; + long val = strtol(buf, &end, 10); + if (errno || end == buf || *end != '\0') + return false; + *out = val; + return true; +} - if ((size_t)(end - start) == klen && - strncmp(start, key, klen) == 0) { - const char *q = skip_ws(end + 1); +static bool json_token_to_double(const jsmntok_t *tok, const char *json, + double *out) +{ + if (!tok || tok->type != JSMN_PRIMITIVE) + return false; + char buf[64]; + int len = tok->end - tok->start; + if (len < 0 || len >= (int)sizeof(buf)) + return false; + memcpy(buf, json + tok->start, len); + buf[len] = '\0'; + char *end; + errno = 0; + double val = strtod(buf, &end); + if (errno || end == buf || *end != '\0') + return false; + *out = val; + return true; +} - if (*q == ':') - return json_copy_token(skip_ws(q + 1), out, outsz); - } - p = end + 1; +static bool json_token_to_bool(const jsmntok_t *tok, const char *json, + bool *out) +{ + if (!tok || tok->type != JSMN_PRIMITIVE) + return false; + int len = tok->end - tok->start; + if (len == 4 && strncmp(json + tok->start, "true", 4) == 0) { + *out = true; + return true; + } + if (len == 5 && strncmp(json + tok->start, "false", 5) == 0) { + *out = false; + return true; } return false; } -/* Same, but requires a plain JSON string and strips the quotes. */ -static bool json_get_string(const char *s, const char *key, char *out, - size_t outsz) +static bool json_token_to_raw(const jsmntok_t *tok, const char *json, + char *out, size_t outsz) { - char token[192]; - size_t len; - - if (!json_find_value(s, key, token, sizeof(token))) + if (!tok || !json || !out || outsz == 0) return false; - if (token[0] != '"') + int len = tok->end - tok->start; + if (len < 0 || (size_t)len >= outsz) return false; - len = strlen(token); - if (len < 2 || token[len - 1] != '"') - return false; - if (len - 1 > outsz) - return false; - memcpy(out, token + 1, len - 2); - out[len - 2] = '\0'; + memcpy(out, json + tok->start, len); + out[len] = '\0'; return true; } -static bool json_get_long(const char *s, const char *key, long *out) +static bool json_parse_request(const char *input, size_t len, + const char *key, char *out, size_t outsz, + json_get_type_e type, void *num_out) { - char token[32]; - char *end; - long value; - - if (!json_find_value(s, key, token, sizeof(token))) + if (len >= IPC_JSON_MAX_INPUT) return false; - if (token[0] == '"' || token[0] == '\0') + + jsmn_parser parser; + jsmntok_t tokens[IPC_JSON_MAX_TOKENS]; + jsmn_init(&parser); + int tok_count = jsmn_parse(&parser, input, len, tokens, IPC_JSON_MAX_TOKENS); + if (tok_count < 1 || tokens[0].type != JSMN_OBJECT) return false; - errno = 0; - value = strtol(token, &end, 10); - if (errno || end == token || *end != '\0') + /* Search for the key anywhere in the JSON token tree */ + int val_idx = json_token_find_key(tokens, tok_count, key, input); + if (val_idx < 0) return false; - *out = value; - return true; + + const jsmntok_t *val = &tokens[val_idx]; + switch (type) { + case JSON_GET_STRING: + return json_token_to_string(val, input, out, outsz); + case JSON_GET_LONG: + return json_token_to_long(val, input, (long *)num_out); + case JSON_GET_DOUBLE: + return json_token_to_double(val, input, (double *)num_out); + case JSON_GET_BOOL: + return json_token_to_bool(val, input, (bool *)num_out); + case JSON_GET_RAW: + return json_token_to_raw(val, input, out, outsz); + } + return false; } -static bool json_get_double(const char *s, const char *key, double *out) +static bool json_get_string(const char *s, const char *key, char *out, size_t outsz) { - char token[48]; - char *end; - double value; - - if (!json_find_value(s, key, token, sizeof(token))) + if (!s || !key || !out || !outsz) return false; - if (token[0] == '"' || token[0] == '\0') + return json_parse_request(s, strlen(s), key, out, outsz, JSON_GET_STRING, NULL); +} + +static bool json_get_long(const char *s, const char *key, long *out) +{ + if (!s || !key || !out) return false; + return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_LONG, out); +} - errno = 0; - value = strtod(token, &end); - if (errno || end == token || *end != '\0') +static bool json_get_double(const char *s, const char *key, double *out) +{ + if (!s || !key || !out) return false; - *out = value; - return true; + return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_DOUBLE, out); } static bool json_get_bool(const char *s, const char *key, bool *out) { - char token[16]; + if (!s || !key || !out) + return false; + return json_parse_request(s, strlen(s), key, NULL, 0, JSON_GET_BOOL, out); +} - if (!json_find_value(s, key, token, sizeof(token))) +/* Legacy wrapper for json_find_value (used in ipc_handle_line for "id" and "method") */ +static bool json_find_value(const char *s, const char *key, char *out, size_t outsz) +{ + if (!s || !key || !out || !outsz) return false; - if (strcmp(token, "true") == 0) { - *out = true; - return true; + /* For "method", extract unquoted string */ + if (strcmp(key, "method") == 0) { + return json_get_string(s, key, out, outsz); } - if (strcmp(token, "false") == 0) { - *out = false; - return true; - } - return false; + /* For "id", return raw token (string value, number, or null) */ + return json_parse_request(s, strlen(s), key, out, outsz, JSON_GET_RAW, NULL); } static void json_escape_into(char *dst, size_t dstsz, const char *src) { size_t o = 0; - if (dstsz == 0) + if (!dst || dstsz == 0) + return; + if (!src) { + dst[0] = '\0'; return; + } while (src && *src && o + 2 < dstsz) { unsigned char ch = (unsigned char)*src++; @@ -347,13 +373,52 @@ static void ipc_send_raw(struct ipc_client *c, const char *data, size_t len) } } +static void json_format_id(char *dst, size_t dstsz, const char *id) +{ + if (!dst || dstsz == 0) + return; + if (!id || !*id || strcmp(id, "null") == 0) { + snprintf(dst, dstsz, "null"); + return; + } + char *end = NULL; + strtol(id, &end, 10); + if (end && *end == '\0') { + snprintf(dst, dstsz, "%s", id); + return; + } + size_t len = strlen(id); + char inner[IPC_ID_MAX]; + const char *to_esc = id; + if (id[0] == '"' && len >= 2 && id[len - 1] == '"') { + size_t in_len = len - 2; + if (in_len >= sizeof(inner)) + in_len = sizeof(inner) - 1; + memcpy(inner, id + 1, in_len); + inner[in_len] = '\0'; + to_esc = inner; + } + if (dstsz >= 3) { + dst[0] = '"'; + json_escape_into(dst + 1, dstsz - 2, to_esc); + size_t elen = strlen(dst); + dst[elen] = '"'; + dst[elen + 1] = '\0'; + } else { + snprintf(dst, dstsz, "null"); + } +} + static void ipc_reply(struct ipc_client *c, const char *id, const char *result_json) { + char esc_id[IPC_ID_MAX * 2]; char buf[IPC_REPLY_MAX]; + + json_format_id(esc_id, sizeof(esc_id), id); int n = snprintf(buf, sizeof(buf), "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":%s}\n", - id ? id : "null", result_json); + esc_id, result_json); if (n > 0 && (size_t)n < sizeof(buf)) ipc_send_raw(c, buf, (size_t)n); @@ -362,11 +427,16 @@ static void ipc_reply(struct ipc_client *c, const char *id, static void ipc_error(struct ipc_client *c, const char *id, int code, const char *message) { - char buf[256]; + char esc_id[IPC_ID_MAX * 2]; + char esc_msg[256]; + char buf[512]; + + json_format_id(esc_id, sizeof(esc_id), id); + json_escape_into(esc_msg, sizeof(esc_msg), message ? message : "error"); int n = snprintf(buf, sizeof(buf), "{\"jsonrpc\":\"2.0\",\"id\":%s," "\"error\":{\"code\":%d,\"message\":\"%s\"}}\n", - id ? id : "null", code, message); + esc_id, code, esc_msg); if (n > 0 && (size_t)n < sizeof(buf)) ipc_send_raw(c, buf, (size_t)n); @@ -550,7 +620,9 @@ static void ipc_send_hello(struct ipc_server *s, struct ipc_client *c, "\"audio.mute.set\",\"audio.channels.set\"," "\"osd.set\",\"osd.position.set\"," "\"aspect.set\",\"mode.set\",\"v4l2.set\"," - "\"hdr.set\",\"scale.set\",\"framegen.set\"]}", + "\"hdr.set\",\"hdr.passthrough\",\"hdr.exposure\"," + "\"scale.set\",\"framegen.set\",\"toast.show\"," + "\"edid.set\",\"hotplug\"]}", GC573_IPC_PROTO_VERSION, s->version, (long)getpid(), path_esc); if (n > 0 && (size_t)n < sizeof(result)) @@ -816,21 +888,30 @@ static void ipc_handle_hdr(struct ipc_client *c, const char *id, struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_HDR }; char mode[16]; double exposure = 0.0; + bool passthrough = false; + + cmd.f = -999.0f; /* -999 means leave current exposure unchanged */ if (!json_get_string(line, "mode", mode, sizeof(mode))) { ipc_error(c, id, -32602, "invalid params: mode required"); return; } - if (!strcasecmp(mode, "auto") || !strcasecmp(mode, "on")) + if (!strcasecmp(mode, "auto") || !strcasecmp(mode, "on")) { cmd.a = 1; - else if (!strcasecmp(mode, "off") || !strcasecmp(mode, "bypass")) + } else if (!strcasecmp(mode, "passthrough")) { + cmd.a = 1; + cmd.b = 1; + } else if (!strcasecmp(mode, "off") || !strcasecmp(mode, "bypass")) { cmd.a = 0; - else { - ipc_error(c, id, -32602, "unknown mode: auto|off"); + } else { + ipc_error(c, id, -32602, "unknown mode: auto|off|passthrough"); return; } + if (json_get_bool(line, "passthrough", &passthrough)) + cmd.b = passthrough ? 1 : 0; + if (json_get_double(line, "exposure", &exposure)) { if (exposure < -2.0) exposure = -2.0; @@ -842,6 +923,27 @@ static void ipc_handle_hdr(struct ipc_client *c, const char *id, ipc_push_and_reply(c, id, &cmd); } +static void ipc_handle_hdr_passthrough(struct ipc_client *c, const char *id, + const char *line) +{ + struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_HDR }; + bool enable = false; + + cmd.f = -999.0f; /* leave exposure unchanged */ + + if (!json_get_bool(line, "enabled", &enable) && + !json_get_bool(line, "passthrough", &enable)) { + char mode[16]; + if (json_get_string(line, "mode", mode, sizeof(mode))) + enable = (!strcasecmp(mode, "on") || !strcasecmp(mode, "true") || + !strcasecmp(mode, "passthrough")); + } + + cmd.a = 1; /* keep HDR active */ + cmd.b = enable ? 1 : 0; + ipc_push_and_reply(c, id, &cmd); +} + static void ipc_handle_framegen(struct ipc_client *c, const char *id, const char *line) { @@ -933,19 +1035,96 @@ static void ipc_handle_v4l2(struct ipc_client *c, const char *id, cmd.a = enabled ? 1 : 0; if (json_get_string(line, "device", device, sizeof(device))) { - /* Only character devices; no traversal, no relative paths. */ - if (strncmp(device, "/dev/", 5) != 0 || - strstr(device, "..") != NULL) { - ipc_error(c, id, -32602, - "device must be an absolute /dev/ path"); + /* Only character devices; allowlist /dev/video[0-9]+ */ + if (strncmp(device, "/dev/video", 10) != 0) { + ipc_error(c, id, -32602, "device must match /dev/video[0-9]+"); return; } + for (const char *p = device + 10; *p; p++) { + if (*p < '0' || *p > '9') { + ipc_error(c, id, -32602, "device must match /dev/video[0-9]+"); + return; + } + } snprintf(cmd.path, sizeof(cmd.path), "%s", device); } ipc_push_and_reply(c, id, &cmd); } +static void ipc_handle_edid_set(struct ipc_client *c, const char *id, + const char *line) +{ + struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_EDID }; + char name[64]; + + if (!json_get_string(line, "profile", name, sizeof(name)) && + !json_get_string(line, "name", name, sizeof(name))) { + ipc_error(c, id, -32602, "invalid params: profile required"); + return; + } + if (!name[0]) { + ipc_error(c, id, -32602, "invalid params: empty profile"); + return; + } + + snprintf(cmd.path, sizeof(cmd.path), "%s", name); + ipc_push_and_reply(c, id, &cmd); +} + +static void ipc_handle_hotplug(struct ipc_client *c, const char *id, + const char *line) +{ + struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_HOTPLUG }; + bool pulse = true; + char action[16]; + + if (json_get_bool(line, "pulse", &pulse)) + cmd.a = pulse ? 1 : 0; + if (json_get_string(line, "action", action, sizeof(action))) + cmd.a = (strcasecmp(action, "off") != 0 && + strcasecmp(action, "disable") != 0 && + strcasecmp(action, "0") != 0); + + ipc_push_and_reply(c, id, &cmd); +} + +static void ipc_handle_hdr_exposure(struct ipc_client *c, const char *id, + const char *line) +{ + struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_HDR_EV }; + double exposure = 0.0; + + if (!json_get_double(line, "exposure", &exposure) && + !json_get_double(line, "ev", &exposure)) { + ipc_error(c, id, -32602, "invalid params: exposure required"); + return; + } + + if (exposure < -2.0) + exposure = -2.0; + if (exposure > 2.0) + exposure = 2.0; + cmd.f = (float)exposure; + ipc_push_and_reply(c, id, &cmd); +} + +static void ipc_handle_toast_show(struct ipc_client *c, const char *id, + const char *line) +{ + struct gc573_ipc_cmd cmd = { .kind = GC573_IPC_CMD_TOAST }; + char msg[64] = { 0 }; + + if (json_get_string(line, "message", msg, sizeof(msg)) || + json_get_string(line, "text", msg, sizeof(msg))) { + snprintf(cmd.path, sizeof(cmd.path), "%s", msg); + } else { + snprintf(cmd.path, sizeof(cmd.path), "Notification"); + } + + ipc_push_and_reply(c, id, &cmd); +} + static void ipc_handle_line(struct ipc_server *s, struct ipc_client *c, const char *line) { @@ -989,10 +1168,20 @@ static void ipc_handle_line(struct ipc_server *s, struct ipc_client *c, ipc_handle_v4l2(c, has_id ? id : NULL, line); } else if (strcmp(method, "hdr.set") == 0) { ipc_handle_hdr(c, has_id ? id : NULL, line); + } else if (strcmp(method, "hdr.passthrough") == 0) { + ipc_handle_hdr_passthrough(c, has_id ? id : NULL, line); + } else if (strcmp(method, "hdr.exposure") == 0) { + ipc_handle_hdr_exposure(c, has_id ? id : NULL, line); + } else if (strcmp(method, "edid.set") == 0) { + ipc_handle_edid_set(c, has_id ? id : NULL, line); + } else if (strcmp(method, "hotplug") == 0) { + ipc_handle_hotplug(c, has_id ? id : NULL, line); } else if (strcmp(method, "scale.set") == 0) { ipc_handle_scale(c, has_id ? id : NULL, line); } else if (strcmp(method, "framegen.set") == 0) { ipc_handle_framegen(c, has_id ? id : NULL, line); + } else if (strcmp(method, "toast.show") == 0) { + ipc_handle_toast_show(c, has_id ? id : NULL, line); } else { ipc_error(c, has_id ? id : NULL, -32601, "method not found"); } diff --git a/tools/opengc573-userd/gc573_user_ipc.h b/tools/opengc573-userd/gc573_user_ipc.h index eef1b59..9b8db54 100644 --- a/tools/opengc573-userd/gc573_user_ipc.h +++ b/tools/opengc573-userd/gc573_user_ipc.h @@ -74,6 +74,10 @@ enum gc573_ipc_mode { GC573_IPC_CMD_SCALE, GC573_IPC_CMD_FRAMEGEN, GC573_IPC_CMD_AUDIO_CHANNELS, + GC573_IPC_CMD_TOAST, + GC573_IPC_CMD_EDID, + GC573_IPC_CMD_HOTPLUG, + GC573_IPC_CMD_HDR_EV, }; /* diff --git a/tools/opengc573-userd/gc573_user_it6664.c b/tools/opengc573-userd/gc573_user_it6664.c index 5ac42eb..0e3161c 100644 --- a/tools/opengc573-userd/gc573_user_it6664.c +++ b/tools/opengc573-userd/gc573_user_it6664.c @@ -2721,3 +2721,17 @@ int gc573_user_it6664_poll(void *bar0, struct gc573_user_it6664_status *status) return 0; } + +int gc573_user_it6664_pulse_hpd(void *bar0) +{ + int ret; + + if (!bar0) + return -EINVAL; + + ret = it6664_rx_set_hpd(bar0, false); + if (ret < 0) + return ret; + usleep(150000); + return it6664_rx_set_hpd(bar0, true); +} diff --git a/tools/opengc573-userd/gc573_user_it6664.h b/tools/opengc573-userd/gc573_user_it6664.h index 8803dbd..119bc17 100644 --- a/tools/opengc573-userd/gc573_user_it6664.h +++ b/tools/opengc573-userd/gc573_user_it6664.h @@ -64,4 +64,10 @@ int gc573_user_it6664_enable_source(void *bar0); int gc573_user_it6664_poll(void *bar0, struct gc573_user_it6664_status *status); +/** + * gc573_user_it6664_pulse_hpd - Pulse upstream HDMI RX HPD to trigger source re-sync + * @bar0: Pointer to mapped BAR0 memory + */ +int gc573_user_it6664_pulse_hpd(void *bar0); + #endif /* GC573_USER_IT6664_H */ diff --git a/tools/opengc573-userd/gc573_user_streamer.c b/tools/opengc573-userd/gc573_user_streamer.c index 36131cf..db6d88e 100644 --- a/tools/opengc573-userd/gc573_user_streamer.c +++ b/tools/opengc573-userd/gc573_user_streamer.c @@ -1370,11 +1370,29 @@ static struct osd_overlay g_osd; static struct osd_overlay g_help; static bool g_help_overlay; +static struct osd_overlay g_toast; +static uint64_t g_toast_until_ms; +static char g_toast_text[64]; + /* Filled by the render loop for the OSD latency readout. */ static double g_osd_dma_ms; /* Integer factor used by the last presented frame (0 = not integer scaled). */ static int g_integer_factor; +static void toast_destroy(void) +{ + if (g_toast.texture) { + SDL_DestroyTexture(g_toast.texture); + g_toast.texture = NULL; + } + g_toast.width = 0; + g_toast.height = 0; + g_toast.scale = 0; + g_toast.text[0] = '\0'; + g_toast_until_ms = 0; + g_toast_text[0] = '\0'; +} + static void osd_destroy(void) { if (g_osd.texture) { @@ -1385,6 +1403,7 @@ static void osd_destroy(void) g_osd.height = 0; g_osd.scale = 0; g_osd.text[0] = '\0'; + toast_destroy(); } static void help_destroy(void) @@ -1726,6 +1745,57 @@ static void help_render(SDL_Renderer *renderer, SDL_Window *window) SDL_RenderCopy(renderer, g_help.texture, NULL, &dst); } +static void toast_trigger(const char *msg) +{ + struct timespec ts; + + if (!msg || !*msg) + return; + snprintf(g_toast_text, sizeof(g_toast_text), "%s", msg); + clock_gettime(CLOCK_MONOTONIC, &ts); + g_toast_until_ms = (uint64_t)ts.tv_sec * 1000U + + (uint64_t)(ts.tv_nsec / 1000000L) + 2500U; +} + +static void toast_render(SDL_Renderer *renderer, SDL_Window *window) +{ + struct timespec ts; + uint64_t now; + int win_w = 0; + int win_h = 0; + int scale; + SDL_Rect dst; + + if (!g_toast_text[0]) + return; + + clock_gettime(CLOCK_MONOTONIC, &ts); + now = (uint64_t)ts.tv_sec * 1000U + (uint64_t)(ts.tv_nsec / 1000000L); + if (now >= g_toast_until_ms) + return; + + SDL_GetWindowSize(window, &win_w, &win_h); + if (win_w <= 0 || win_h <= 0) + return; + + scale = osd_scale_for_height(win_h); + if (scale != g_toast.scale || strcmp(g_toast.text, g_toast_text) != 0) + osd_build(renderer, scale, g_toast_text, &g_toast); + if (!g_toast.texture) + return; + + dst.w = g_toast.width; + dst.h = g_toast.height; + dst.x = (win_w - dst.w) / 2; + dst.y = win_h - dst.h - (scale >= 3 ? 36 : (scale == 2 ? 24 : 16)); + if (dst.x < 0) + dst.x = 0; + if (dst.y < 0) + dst.y = 0; + + SDL_RenderCopy(renderer, g_toast.texture, NULL, &dst); +} + static void fit_destination(SDL_Rect *dst, int win_w, int win_h, float src_aspect) { @@ -1841,6 +1911,7 @@ static void render_frame(SDL_Window *window, SDL_Renderer *renderer, SDL_RenderCopy(renderer, texture, NULL, &dst); osd_render(renderer, window); help_render(renderer, window); + toast_render(renderer, window); SDL_RenderPresent(renderer); } @@ -5419,6 +5490,7 @@ static void hdr_apply(const struct gc573_user_video_signal *sig, } static void apply_ipc_command(const struct gc573_ipc_cmd *cmd, + void *bar0, SDL_Window *window, struct v4l2_sink *sink, uint32_t out_w, uint32_t out_h, @@ -5505,12 +5577,41 @@ static void apply_ipc_command(const struct gc573_ipc_cmd *cmd, break; case GC573_IPC_CMD_HDR: g_hdr_enabled = cmd->a != 0; - g_hdr_exposure_ev = cmd->f; + if (cmd->f >= -2.0f && cmd->f <= 2.0f) + g_hdr_exposure_ev = cmd->f; hdr_apply(sig, have_signal); printf("[IPC] HDR: %s, exposure %+.1f EV\n", g_hdr_enabled ? "auto" : "bypass", (double)g_hdr_exposure_ev); break; + case GC573_IPC_CMD_HDR_EV: + if (cmd->f >= -2.0f && cmd->f <= 2.0f) + g_hdr_exposure_ev = cmd->f; + hdr_apply(sig, have_signal); + printf("[IPC] HDR exposure: %+.1f EV\n", + (double)g_hdr_exposure_ev); + break; + case GC573_IPC_CMD_TOAST: + toast_trigger(cmd->path); + printf("[IPC] Toast: '%s'\n", cmd->path); + break; + case GC573_IPC_CMD_EDID: + if (bar0) { + int ret = gc573_user_program_edid_by_name(bar0, cmd->path, NULL); + if (ret == 0) { + (void)gc573_user_it6664_enable_source(bar0); + printf("[IPC] EDID profile set: %s\n", cmd->path); + } else { + pr_warn("IPC EDID profile set '%s' failed: %d", cmd->path, ret); + } + } + break; + case GC573_IPC_CMD_HOTPLUG: + if (bar0) { + (void)gc573_user_it6664_pulse_hpd(bar0); + printf("[IPC] HDMI HPD pulsed (source resync)\n"); + } + break; case GC573_IPC_CMD_SCALE: g_scale_mode = cmd->a ? SCALE_INTEGER : SCALE_FIT; /* The texture is updated by render_frame() via g_scale_applied. */ @@ -6308,7 +6409,7 @@ int main(int argc, char *argv[]) * ever appends to the lock-free command ring. */ while (gc573_ipc_poll_cmd(&ipc_cmd)) - apply_ipc_command(&ipc_cmd, window, &v4l2_sink, + apply_ipc_command(&ipc_cmd, ctx.bar0, window, &v4l2_sink, output_width, output_height, ¤t_sig, have_signal); diff --git a/tools/opengc573-userd/third_party/jsmn.c b/tools/opengc573-userd/third_party/jsmn.c new file mode 100644 index 0000000..48867e4 --- /dev/null +++ b/tools/opengc573-userd/third_party/jsmn.c @@ -0,0 +1,4 @@ +/* jsmn implementation - compiled with parent links */ +#define JSMN_PARENT_LINKS +#undef JSMN_HEADER +#include "jsmn.h" diff --git a/tools/opengc573-userd/third_party/jsmn.h b/tools/opengc573-userd/third_party/jsmn.h new file mode 100644 index 0000000..8ac14c1 --- /dev/null +++ b/tools/opengc573-userd/third_party/jsmn.h @@ -0,0 +1,471 @@ +/* + * MIT License + * + * Copyright (c) 2010 Serge Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#ifndef JSMN_H +#define JSMN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef JSMN_STATIC +#define JSMN_API static +#else +#define JSMN_API extern +#endif + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_UNDEFINED = 0, + JSMN_OBJECT = 1 << 0, + JSMN_ARRAY = 1 << 1, + JSMN_STRING = 1 << 2, + JSMN_PRIMITIVE = 1 << 3 +} jsmntype_t; + +enum jsmnerr { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3 +}; + +/** + * JSON token description. + * type type (object, array, string etc.) + * start start position in JSON data string + * end end position in JSON data string + */ +typedef struct jsmntok { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string. + */ +typedef struct jsmn_parser { + unsigned int pos; /* offset in the JSON string */ + unsigned int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g. parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +JSMN_API void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each + * describing + * a single JSON object. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens); + +#ifndef JSMN_HEADER +/** + * Allocates a fresh unused token from the token pool. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, + const int start, const int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t': + case '\r': + case '\n': + case ' ': + case ',': + case ']': + case '}': + goto found; + default: + /* to quiet a warning from gcc*/ + break; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + if (tokens == NULL) { + parser->pos--; + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return 0; +} + +/** + * Fills next token with JSON string. + */ +static int jsmn_parse_string(jsmn_parser *parser, const char *js, + const size_t len, jsmntok_t *tokens, + const size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + /* Skip starting quote */ + parser->pos++; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + if (tokens == NULL) { + return 0; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return 0; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\' && parser->pos + 1 < len) { + int i; + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': + case '/': + case '\\': + case 'b': + case 'f': + case 'r': + case 'n': + case 't': + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + parser->pos++; + for (i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; + i++) { + /* If it isn't a hex character we have an error */ + if (!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */ + (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */ + (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */ + parser->pos = start; + return JSMN_ERROR_INVAL; + } + parser->pos++; + } + parser->pos--; + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, + jsmntok_t *tokens, const unsigned int num_tokens) { + int r; + int i; + jsmntok_t *token; + int count = parser->toknext; + + for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': + case '[': + count++; + if (tokens == NULL) { + break; + } + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + return JSMN_ERROR_NOMEM; + } + if (parser->toksuper != -1) { + jsmntok_t *t = &tokens[parser->toksuper]; +#ifdef JSMN_STRICT + /* In strict mode an object or array can't become a key */ + if (t->type == JSMN_OBJECT) { + return JSMN_ERROR_INVAL; + } +#endif + t->size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': + case ']': + if (tokens == NULL) { + break; + } + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + if (token->type != type || parser->toksuper == -1) { + return JSMN_ERROR_INVAL; + } + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) { + return JSMN_ERROR_INVAL; + } + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + case '\t': + case '\r': + case '\n': + case ' ': + break; + case ':': + parser->toksuper = parser->toknext - 1; + break; + case ',': + if (tokens != NULL && parser->toksuper != -1 && + tokens[parser->toksuper].type != JSMN_ARRAY && + tokens[parser->toksuper].type != JSMN_OBJECT) { +#ifdef JSMN_PARENT_LINKS + parser->toksuper = tokens[parser->toksuper].parent; +#else + for (i = parser->toknext - 1; i >= 0; i--) { + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + if (tokens[i].start != -1 && tokens[i].end == -1) { + parser->toksuper = i; + break; + } + } + } +#endif + } + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 't': + case 'f': + case 'n': + /* And they must not be keys of the object */ + if (tokens != NULL && parser->toksuper != -1) { + const jsmntok_t *t = &tokens[parser->toksuper]; + if (t->type == JSMN_OBJECT || + (t->type == JSMN_STRING && t->size != 0)) { + return JSMN_ERROR_INVAL; + } + } +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); + if (r < 0) { + return r; + } + count++; + if (parser->toksuper != -1 && tokens != NULL) { + tokens[parser->toksuper].size++; + } + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + } + } + + if (tokens != NULL) { + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + } + + return count; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +JSMN_API void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + +#endif /* JSMN_HEADER */ + +#ifdef __cplusplus +} +#endif + +#endif /* JSMN_H */