fix(cypher): reject unconsumed query tokens

cbm_parse previously returned success when parse_return_item stopped before postfix list indexing or an arbitrary trailing token. A query containing labels(n)[0] could therefore ignore its alias, aggregate, ordering, and limit and execute as an unbounded projection.

Require TOK_EOF after parse_post_where in src/cypher/cypher.c and transfer the parent cursor after a recursively validated UNION tail. Route both list-index rejection paths through one diagnostic that gives supported labels(n) and n.label/count(*) rewrites.

Add parser regressions in tests/test_cypher.c. Verified 155/155 Cypher tests under ASan/UBSan, all three UNION tests, source-safety, an optimized CLI rejection, and the supported five-row node-label aggregation.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-18 18:50:08 -04:00
parent 7dd1e27677
commit a4b3a71e81
2 changed files with 54 additions and 2 deletions
+23 -2
View File
@@ -1548,6 +1548,13 @@ static int parse_string_func_item(parser_t *p, cbm_return_item_t *item) {
return 0;
}
static void set_unsupported_list_index_error(parser_t *p) {
snprintf(p->error, sizeof(p->error),
"unsupported expression: list indexing/slicing '[...]' is not supported; return "
"labels(n) AS labels directly, or group scalar node labels with RETURN n.label AS "
"label, count(*) AS node_count ORDER BY node_count DESC LIMIT 5");
}
static int parse_return_item(parser_t *p, cbm_return_item_t *item) {
memset(item, 0, sizeof(*item));
int rc = 0;
@@ -1583,8 +1590,7 @@ static int parse_return_item(parser_t *p, cbm_return_item_t *item) {
"trim, ltrim, rtrim, reverse, labels, type, id, keys, properties)",
item->variable ? item->variable : "?");
} else {
snprintf(p->error, sizeof(p->error),
"unsupported expression: list indexing/slicing '[...]' is not supported");
set_unsupported_list_index_error(p);
}
safe_str_free(&item->variable);
safe_str_free(&item->property);
@@ -1880,6 +1886,8 @@ static int parse_post_where(parser_t *p, cbm_query_t *q, // NOLINT(misc-no-recur
q->union_next = sub.query;
sub.query = NULL;
cbm_parse_free(&sub);
/* The recursive parser owns and validates the complete UNION tail. */
p->pos = p->count - SKIP_ONE;
}
return 0;
}
@@ -1943,6 +1951,19 @@ int cbm_parse(const cbm_token_t *tokens, int token_count, // NOLINT(misc-no-recu
return CBM_NOT_FOUND;
}
if (!check(&p, TOK_EOF)) {
if (check(&p, TOK_LBRACKET)) {
set_unsupported_list_index_error(&p);
out->error = heap_strdup(p.error);
} else {
snprintf(p.error, sizeof(p.error), "unexpected trailing token '%s' at pos %d",
peek(&p)->text, peek(&p)->pos);
out->error = heap_strdup(p.error);
}
cbm_query_free(q);
return CBM_NOT_FOUND;
}
out->query = q;
return 0;
}
+31
View File
@@ -2769,6 +2769,35 @@ TEST(cypher_issue240_labels_function) {
PASS();
}
TEST(cypher_rejects_list_index_after_function_result) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (n) RETURN labels(n)[0] AS label, count(*) AS count "
"ORDER BY count DESC LIMIT 5",
&q, &err);
ASSERT(rc != 0);
ASSERT_NULL(q);
ASSERT_NOT_NULL(err);
ASSERT_NOT_NULL(strstr(err, "list indexing"));
ASSERT_NOT_NULL(strstr(err, "labels(n) AS labels"));
ASSERT_NOT_NULL(strstr(err, "n.label AS label"));
ASSERT_NOT_NULL(strstr(err, "count(*) AS node_count"));
free(err);
PASS();
}
TEST(cypher_rejects_unconsumed_trailing_tokens) {
cbm_query_t *q = NULL;
char *err = NULL;
int rc = cbm_cypher_parse("MATCH (n) RETURN n.name trailing", &q, &err);
ASSERT(rc != 0);
ASSERT_NULL(q);
ASSERT_NOT_NULL(err);
ASSERT_NOT_NULL(strstr(err, "unexpected trailing token"));
free(err);
PASS();
}
/* #237: DISTINCT applied before ORDER BY + LIMIT */
TEST(cypher_issue237_distinct_order_limit) {
cbm_store_t *s = setup_cypher_store();
@@ -2969,6 +2998,8 @@ SUITE(cypher) {
/* Execution */
RUN_TEST(cypher_exec_match_all_functions);
RUN_TEST(cypher_issue240_labels_function);
RUN_TEST(cypher_rejects_list_index_after_function_result);
RUN_TEST(cypher_rejects_unconsumed_trailing_tokens);
RUN_TEST(cypher_issue237_distinct_order_limit);
RUN_TEST(cypher_issue873_distinct_order_limit_dedupes_before_limit);
RUN_TEST(cypher_issue873_distinct_limit_dedupes_before_limit);