mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-07 08:43:35 +08:00
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>
This commit is contained in:
+36
-2
@@ -2230,15 +2230,49 @@ static const char *json_extract_prop(const char *json, const char *key, char *bu
|
||||
p++;
|
||||
}
|
||||
if (*p == '"') {
|
||||
/* String value */
|
||||
/* String value — honor backslash escapes: without this, an embedded \"
|
||||
* cuts the value short at the first escaped quote. */
|
||||
p++;
|
||||
size_t i = 0;
|
||||
while (*p && *p != '"' && i < buf_sz - SKIP_ONE) {
|
||||
if (*p == '\\' && p[SKIP_ONE] && i + SKIP_ONE < buf_sz - SKIP_ONE) {
|
||||
buf[i++] = *p++; /* keep the escape pair intact */
|
||||
}
|
||||
buf[i++] = *p++;
|
||||
}
|
||||
buf[i] = '\0';
|
||||
} else if (*p == '[' || *p == '{') {
|
||||
/* Array/object value — copy the whole balanced construct. A scan-to-comma
|
||||
* truncates at the first comma INSIDE the value: e.g. a decorators array
|
||||
* ["@Roles('OWNER', 'ADMIN')","@Get()"] came back as ["@Roles('OWNER'. */
|
||||
char open = *p;
|
||||
char close = (open == '[') ? ']' : '}';
|
||||
int depth = 0;
|
||||
int in_str = 0;
|
||||
size_t i = 0;
|
||||
while (*p && i < buf_sz - SKIP_ONE) {
|
||||
char c = *p;
|
||||
if (in_str) {
|
||||
if (c == '\\' && p[SKIP_ONE] && i + SKIP_ONE < buf_sz - SKIP_ONE) {
|
||||
buf[i++] = *p++; /* escape pair stays intact */
|
||||
} else if (c == '"') {
|
||||
in_str = 0;
|
||||
}
|
||||
} else if (c == '"') {
|
||||
in_str = 1;
|
||||
} else if (c == open) {
|
||||
depth++;
|
||||
} else if (c == close) {
|
||||
depth--;
|
||||
}
|
||||
buf[i++] = *p++;
|
||||
if (!in_str && depth == 0) {
|
||||
break; /* outer bracket closed */
|
||||
}
|
||||
}
|
||||
buf[i] = '\0';
|
||||
} else {
|
||||
/* Numeric or other value */
|
||||
/* Numeric or other scalar value */
|
||||
size_t i = 0;
|
||||
while (*p && *p != ',' && *p != '}' && *p != ' ' && i < buf_sz - SKIP_ONE) {
|
||||
buf[i++] = *p++;
|
||||
|
||||
@@ -2794,6 +2794,59 @@ TEST(cypher_multi_prop_projection_no_alias) {
|
||||
* forked child so a stack overwrite (ASan abort, or a raw segfault) shows up
|
||||
* as a killing signal instead of taking down the whole runner; the bounded
|
||||
* path returns an ordinary error and the child exits cleanly. */
|
||||
/* Property projection must return the WHOLE value of composite properties.
|
||||
* json_extract_prop() scanned a non-string value up to the first ',' — so an
|
||||
* array/object property was truncated at its first INTERNAL comma. Real-world
|
||||
* hit: a NestJS handler's decorators
|
||||
* ["@Roles('OWNER', 'ADMIN')","@Get()"]
|
||||
* projected as ["@Roles('OWNER' — unusable for route/authz queries. */
|
||||
TEST(cypher_exec_prop_array_with_internal_commas) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Method",
|
||||
.name = "findAll",
|
||||
.qualified_name = "test.PacienteController.findAll",
|
||||
.file_path = "paciente.controller.ts",
|
||||
.properties_json =
|
||||
"{\"decorators\":[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"],\"lines\":3}"};
|
||||
cbm_store_upsert_node(s, &n);
|
||||
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s, "MATCH (m:Method) RETURN m.decorators, m.lines", "test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
/* whole array, commas and all — was ["@Roles('OWNER' before the fix */
|
||||
ASSERT_STR_EQ(r.rows[0][0], "[\"@Roles('OWNER', 'ADMIN')\",\"@Get()\"]");
|
||||
ASSERT_STR_EQ(r.rows[0][1], "3"); /* scalar sibling still parses */
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A string property must not end at an ESCAPED quote: the scan stopped at the
|
||||
* first '"' regardless of a preceding backslash, cutting the value short. */
|
||||
TEST(cypher_exec_prop_string_with_escaped_quote) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
cbm_node_t n = {.project = "test",
|
||||
.label = "Function",
|
||||
.name = "parse",
|
||||
.qualified_name = "test.parse",
|
||||
.file_path = "p.ts",
|
||||
.properties_json = "{\"signature\":\"(sep: \\\"a,b\\\") => void\"}"};
|
||||
cbm_store_upsert_node(s, &n);
|
||||
|
||||
cbm_cypher_result_t r = {0};
|
||||
int rc = cbm_cypher_execute(s, "MATCH (f:Function) RETURN f.signature", "test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.row_count, 1);
|
||||
ASSERT_STR_EQ(r.rows[0][0], "(sep: \\\"a,b\\\") => void"); /* was: (sep: \ */
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_wide_return_projection_bounded) {
|
||||
#ifdef _WIN32
|
||||
SKIP_PLATFORM("fork crash-isolation is POSIX-only; the parse-time bound is platform-agnostic");
|
||||
@@ -2994,4 +3047,7 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_parse_unwind);
|
||||
RUN_TEST(cypher_parse_unwind_var);
|
||||
RUN_TEST(cypher_wide_return_projection_bounded);
|
||||
/* Composite property projection (arrays/objects, escaped quotes) */
|
||||
RUN_TEST(cypher_exec_prop_array_with_internal_commas);
|
||||
RUN_TEST(cypher_exec_prop_string_with_escaped_quote);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user