Attach source or context lines to a search result JSON item. */
| 8904 | |
| 8905 | /* Attach source or context lines to a search result JSON item. */ |
| 8906 | static void attach_result_source(yyjson_mut_doc *doc, yyjson_mut_val *item, search_result_t *r, |
| 8907 | int mode, int context_lines, const char *root_path) { |
| 8908 | enum { MODE_FULL = 1 }; |
| 8909 | if (r->start_line <= 0 || r->end_line <= 0) { |
| 8910 | return; |
| 8911 | } |
| 8912 | char abs_path[CBM_SZ_1K]; |
| 8913 | snprintf(abs_path, sizeof(abs_path), "%s/%s", root_path, r->file); |
| 8914 | |
| 8915 | /* Containment: a search result whose indexed path resolves outside the |
| 8916 | * project root (a `..` segment, or a symlink/junction that discovery |
| 8917 | * followed) must not be read back into the response. Same guard the |
| 8918 | * snippet path already uses. */ |
| 8919 | if (!cbm_path_within_root(root_path, abs_path)) { |
| 8920 | return; |
| 8921 | } |
| 8922 | |
| 8923 | if (mode == MODE_FULL) { |
| 8924 | /* Cap each hit's source at a match-anchored window: uncapped |
| 8925 | * whole-symbol dumps ran to 5.7KB × N hits (142KB responses). The |
| 8926 | * complete symbol stays one get_code_snippet call away; |
| 8927 | * source_start/source_truncated make the cut explicit. */ |
| 8928 | enum { SC_FULL_MAX_LINES = 60, SC_FULL_LEAD = 5 }; |
| 8929 | int s = r->start_line; |
| 8930 | int e = r->end_line; |
| 8931 | bool truncated = false; |
| 8932 | if (e - s + 1 > SC_FULL_MAX_LINES) { |
| 8933 | if (r->match_count > 0 && r->match_lines[0] - SC_FULL_LEAD > s) { |
| 8934 | s = r->match_lines[0] - SC_FULL_LEAD; |
| 8935 | } |
| 8936 | e = s + SC_FULL_MAX_LINES - 1; |
| 8937 | if (e > r->end_line) { |
| 8938 | e = r->end_line; |
| 8939 | } |
| 8940 | truncated = true; |
| 8941 | } |
| 8942 | char *source = read_file_lines(abs_path, s, e); |
| 8943 | if (source) { |
| 8944 | sanitize_ascii(source); |
| 8945 | yyjson_mut_obj_add_strcpy(doc, item, "source", source); |
| 8946 | free(source); |
| 8947 | if (truncated) { |
| 8948 | yyjson_mut_obj_add_int(doc, item, "source_start", s); |
| 8949 | yyjson_mut_obj_add_bool(doc, item, "source_truncated", true); |
| 8950 | } |
| 8951 | } |
| 8952 | } else if (context_lines > 0 && r->match_count > 0) { |
| 8953 | int ctx_start = r->match_lines[0] - context_lines; |
| 8954 | int ctx_end = r->match_lines[r->match_count - SKIP_ONE] + context_lines; |
| 8955 | if (ctx_start < SKIP_ONE) { |
| 8956 | ctx_start = SKIP_ONE; |
| 8957 | } |
| 8958 | char *ctx = read_file_lines(abs_path, ctx_start, ctx_end); |
| 8959 | if (ctx) { |
| 8960 | sanitize_ascii(ctx); |
| 8961 | yyjson_mut_obj_add_strcpy(doc, item, "context", ctx); |
| 8962 | yyjson_mut_obj_add_int(doc, item, "context_start", ctx_start); |
| 8963 | free(ctx); |
no test coverage detected