Skip to content

Commit 48fa9cb

Browse files
fix(cypher): return whole value for composite and escaped properties
json_extract_prop() scanned a non-string property up to the first ',' and a string property up to the first '"', ignoring nesting and backslash escapes. Any array/object property was therefore truncated at its first INTERNAL comma, and any string containing an escaped quote was cut short. decorators: ["@roles('OWNER', 'ADMIN')","@get()"] projected as: ["@roles('OWNER' This makes decorator/route/authz queries unusable on frameworks whose decorators take multiple arguments (NestJS, Angular, Spring). Values are now copied as balanced constructs, honoring string state and escape pairs; scalar and plain-string paths are unchanged. Signed-off-by: KolisCode <jhohantma@gmail.com>
1 parent feadbe1 commit 48fa9cb

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

src/cypher/cypher.c

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2230,15 +2230,49 @@ static const char *json_extract_prop(const char *json, const char *key, char *bu
22302230
p++;
22312231
}
22322232
if (*p == '"') {
2233-
/* String value */
2233+
/* String value — honor backslash escapes: without this, an embedded \"
2234+
* cuts the value short at the first escaped quote. */
22342235
p++;
22352236
size_t i = 0;
22362237
while (*p && *p != '"' && i < buf_sz - SKIP_ONE) {
2238+
if (*p == '\\' && p[SKIP_ONE] && i + SKIP_ONE < buf_sz - SKIP_ONE) {
2239+
buf[i++] = *p++; /* keep the escape pair intact */
2240+
}
2241+
buf[i++] = *p++;
2242+
}
2243+
buf[i] = '\0';
2244+
} else if (*p == '[' || *p == '{') {
2245+
/* Array/object value — copy the whole balanced construct. A scan-to-comma
2246+
* truncates at the first comma INSIDE the value: e.g. a decorators array
2247+
* ["@Roles('OWNER', 'ADMIN')","@Get()"] came back as ["@Roles('OWNER'. */
2248+
char open = *p;
2249+
char close = (open == '[') ? ']' : '}';
2250+
int depth = 0;
2251+
int in_str = 0;
2252+
size_t i = 0;
2253+
while (*p && i < buf_sz - SKIP_ONE) {
2254+
char c = *p;
2255+
if (in_str) {
2256+
if (c == '\\' && p[SKIP_ONE] && i + SKIP_ONE < buf_sz - SKIP_ONE) {
2257+
buf[i++] = *p++; /* escape pair stays intact */
2258+
} else if (c == '"') {
2259+
in_str = 0;
2260+
}
2261+
} else if (c == '"') {
2262+
in_str = 1;
2263+
} else if (c == open) {
2264+
depth++;
2265+
} else if (c == close) {
2266+
depth--;
2267+
}
22372268
buf[i++] = *p++;
2269+
if (!in_str && depth == 0) {
2270+
break; /* outer bracket closed */
2271+
}
22382272
}
22392273
buf[i] = '\0';
22402274
} else {
2241-
/* Numeric or other value */
2275+
/* Numeric or other scalar value */
22422276
size_t i = 0;
22432277
while (*p && *p != ',' && *p != '}' && *p != ' ' && i < buf_sz - SKIP_ONE) {
22442278
buf[i++] = *p++;

tests/test_cypher.c

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2794,6 +2794,59 @@ TEST(cypher_multi_prop_projection_no_alias) {
27942794
* forked child so a stack overwrite (ASan abort, or a raw segfault) shows up
27952795
* as a killing signal instead of taking down the whole runner; the bounded
27962796
* path returns an ordinary error and the child exits cleanly. */
2797+
/* Property projection must return the WHOLE value of composite properties.
2798+
* json_extract_prop() scanned a non-string value up to the first ',' — so an
2799+
* array/object property was truncated at its first INTERNAL comma. Real-world
2800+
* hit: a NestJS handler's decorators
2801+
* ["@Roles('OWNER', 'ADMIN')","@Get()"]
2802+
* projected as ["@Roles('OWNER' — unusable for route/authz queries. */
2803+
TEST(cypher_exec_prop_array_with_internal_commas) {
2804+
cbm_store_t *s = cbm_store_open_memory();
2805+
cbm_store_upsert_project(s, "test", "/tmp/test");
2806+
cbm_node_t n = {.project = "test",
2807+
.label = "Method",
2808+
.name = "findAll",
2809+
.qualified_name = "test.PacienteController.findAll",
2810+
.file_path = "paciente.controller.ts",
2811+
.properties_json =
2812+
"{\"decorators\":[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"],\"lines\":3}"};
2813+
cbm_store_upsert_node(s, &n);
2814+
2815+
cbm_cypher_result_t r = {0};
2816+
int rc = cbm_cypher_execute(s, "MATCH (m:Method) RETURN m.decorators, m.lines", "test", 0, &r);
2817+
ASSERT_EQ(rc, 0);
2818+
ASSERT_EQ(r.row_count, 1);
2819+
/* whole array, commas and all — was ["@Roles('OWNER' before the fix */
2820+
ASSERT_STR_EQ(r.rows[0][0], "[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"]");
2821+
ASSERT_STR_EQ(r.rows[0][1], "3"); /* scalar sibling still parses */
2822+
cbm_cypher_result_free(&r);
2823+
cbm_store_close(s);
2824+
PASS();
2825+
}
2826+
2827+
/* A string property must not end at an ESCAPED quote: the scan stopped at the
2828+
* first '"' regardless of a preceding backslash, cutting the value short. */
2829+
TEST(cypher_exec_prop_string_with_escaped_quote) {
2830+
cbm_store_t *s = cbm_store_open_memory();
2831+
cbm_store_upsert_project(s, "test", "/tmp/test");
2832+
cbm_node_t n = {.project = "test",
2833+
.label = "Function",
2834+
.name = "parse",
2835+
.qualified_name = "test.parse",
2836+
.file_path = "p.ts",
2837+
.properties_json = "{\"signature\":\"(sep: \\\"a,b\\\") => void\"}"};
2838+
cbm_store_upsert_node(s, &n);
2839+
2840+
cbm_cypher_result_t r = {0};
2841+
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.signature", "test", 0, &r);
2842+
ASSERT_EQ(rc, 0);
2843+
ASSERT_EQ(r.row_count, 1);
2844+
ASSERT_STR_EQ(r.rows[0][0], "(sep: \\\"a,b\\\") => void"); /* was: (sep: \ */
2845+
cbm_cypher_result_free(&r);
2846+
cbm_store_close(s);
2847+
PASS();
2848+
}
2849+
27972850
TEST(cypher_wide_return_projection_bounded) {
27982851
#ifdef _WIN32
27992852
SKIP_PLATFORM("fork crash-isolation is POSIX-only; the parse-time bound is platform-agnostic");
@@ -2994,4 +3047,7 @@ SUITE(cypher) {
29943047
RUN_TEST(cypher_parse_unwind);
29953048
RUN_TEST(cypher_parse_unwind_var);
29963049
RUN_TEST(cypher_wide_return_projection_bounded);
3050+
/* Composite property projection (arrays/objects, escaped quotes) */
3051+
RUN_TEST(cypher_exec_prop_array_with_internal_commas);
3052+
RUN_TEST(cypher_exec_prop_string_with_escaped_quote);
29973053
}

0 commit comments

Comments
 (0)