diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index b83cf5b80..676f1b169 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -103,7 +103,9 @@ static int def_json_escape_char(char *buf, size_t avail, char ch) { break; default: if (avail >= SKIP_ONE) { - buf[0] = ch; + /* Any other raw control byte (e.g. form feed) is invalid inside a + * JSON string — degrade to a space. */ + buf[0] = ((unsigned char)ch < 0x20) ? ' ' : ch; } return SKIP_ONE; } @@ -194,14 +196,11 @@ static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; } + /* Full escaping (not just quote/backslash): items like C param types + * sliced from multi-line declarations carry raw \n/\t bytes, which are + * invalid inside JSON strings. */ for (const char *s = arr[i]; *s && p < bufsize - PD_ESC_SPACE; s++) { - if (*s == '"' || *s == '\\') { - buf[p++] = '\\'; - if (p >= bufsize - PD_ESC_SPACE) { - break; - } - } - buf[p++] = *s; + p += (size_t)def_json_escape_char(buf + p, bufsize - p - PD_ESC_SPACE, *s); } if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 4713b77c6..91dd5601f 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -145,7 +145,9 @@ static int json_escape_char(char *buf, size_t avail, char ch) { break; default: if (avail >= SKIP_ONE) { - buf[0] = ch; + /* Any other raw control byte (e.g. form feed) is invalid inside a + * JSON string — degrade to a space. */ + buf[0] = ((unsigned char)ch < 0x20) ? ' ' : ch; } return SKIP_ONE; } @@ -237,14 +239,11 @@ static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; } + /* Full escaping (not just quote/backslash): items like C param types + * sliced from multi-line declarations carry raw \n/\t bytes, which are + * invalid inside JSON strings. */ for (const char *s = arr[i]; *s && p < bufsize - PP_ESC_SPACE; s++) { - if (*s == '"' || *s == '\\') { - buf[p++] = '\\'; - if (p >= bufsize - PP_ESC_SPACE) { - break; - } - } - buf[p++] = *s; + p += (size_t)json_escape_char(buf + p, bufsize - p - PP_ESC_SPACE, *s); } if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index d2ed468fd..333865229 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -506,6 +506,23 @@ TEST(pipeline_def_props_valid_json_when_oversized) { fclose(f); } + /* Multi-line parameter declarations: param_types items then carry raw + * newline/tab bytes from the source slice. The array appender's inline + * escape loop only handled quote/backslash, so the raw control bytes made + * the JSON invalid (118 Linux-kernel rows of this shape). */ + { + char path[512]; + snprintf(path, sizeof(path), "%s/sweep_nl.c", g_tmpdir); + FILE *f = fopen(path, "w"); + if (!f) { + teardown_test_repo(); + FAIL("failed to write sweep_nl.c"); + } + fprintf(f, "int sweep_fn_nl(struct\n\t\t\t\treally_long_struct_name *a,\n" + " enum\n\tweird_enum b) { return 0; }\n"); + fclose(f); + } + char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_huge_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); @@ -539,7 +556,7 @@ TEST(pipeline_def_props_valid_json_when_oversized) { yyjson_doc_free(doc); checked++; } - ASSERT_EQ(checked, 60); /* all sweep functions present */ + ASSERT_EQ(checked, 61); /* all sweep functions present (60 sizes + nl case) */ cbm_store_free_nodes(funcs, func_count); cbm_store_close(s);