Attach source or context lines to a search result JSON item. */
| 8855 | |
| 8856 | /* Attach source or context lines to a search result JSON item. */ |
| 8857 | static void attach_result_source(yyjson_mut_doc *doc, yyjson_mut_val *item, search_result_t *r, |
| 8858 | int mode, int context_lines, const char *root_path) { |
| 8859 | enum { MODE_FULL = 1 }; |
| 8860 | if (r->start_line <= 0 || r->end_line <= 0) { |
| 8861 | return; |
| 8862 | } |
| 8863 | char abs_path[CBM_SZ_1K]; |
| 8864 | snprintf(abs_path, sizeof(abs_path), "%s/%s", root_path, r->file); |
| 8865 | |
| 8866 | /* Containment: a search result whose indexed path resolves outside the |
| 8867 | * project root (a `..` segment, or a symlink/junction that discovery |
| 8868 | * followed) must not be read back into the response. Same guard the |
| 8869 | * snippet path already uses. */ |
| 8870 | if (!cbm_path_within_root(root_path, abs_path)) { |
| 8871 | return; |
| 8872 | } |
| 8873 | |
| 8874 | if (mode == MODE_FULL) { |
| 8875 | /* Cap each hit's source at a match-anchored window: uncapped |
| 8876 | * whole-symbol dumps ran to 5.7KB × N hits (142KB responses). The |
| 8877 | * complete symbol stays one get_code_snippet call away; |
| 8878 | * source_start/source_truncated make the cut explicit. */ |
| 8879 | enum { SC_FULL_MAX_LINES = 60, SC_FULL_LEAD = 5 }; |
| 8880 | int s = r->start_line; |
| 8881 | int e = r->end_line; |
| 8882 | bool truncated = false; |
| 8883 | if (e - s + 1 > SC_FULL_MAX_LINES) { |
| 8884 | if (r->match_count > 0 && r->match_lines[0] - SC_FULL_LEAD > s) { |
| 8885 | s = r->match_lines[0] - SC_FULL_LEAD; |
| 8886 | } |
| 8887 | e = s + SC_FULL_MAX_LINES - 1; |
| 8888 | if (e > r->end_line) { |
| 8889 | e = r->end_line; |
| 8890 | } |
| 8891 | truncated = true; |
| 8892 | } |
| 8893 | char *source = read_file_lines(abs_path, s, e); |
| 8894 | if (source) { |
| 8895 | sanitize_ascii(source); |
| 8896 | yyjson_mut_obj_add_strcpy(doc, item, "source", source); |
| 8897 | free(source); |
| 8898 | if (truncated) { |
| 8899 | yyjson_mut_obj_add_int(doc, item, "source_start", s); |
| 8900 | yyjson_mut_obj_add_bool(doc, item, "source_truncated", true); |
| 8901 | } |
| 8902 | } |
| 8903 | } else if (context_lines > 0 && r->match_count > 0) { |
| 8904 | int ctx_start = r->match_lines[0] - context_lines; |
| 8905 | int ctx_end = r->match_lines[r->match_count - SKIP_ONE] + context_lines; |
| 8906 | if (ctx_start < SKIP_ONE) { |
| 8907 | ctx_start = SKIP_ONE; |
| 8908 | } |
| 8909 | char *ctx = read_file_lines(abs_path, ctx_start, ctx_end); |
| 8910 | if (ctx) { |
| 8911 | sanitize_ascii(ctx); |
| 8912 | yyjson_mut_obj_add_strcpy(doc, item, "context", ctx); |
| 8913 | yyjson_mut_obj_add_int(doc, item, "context_start", ctx_start); |
| 8914 | free(ctx); |
no test coverage detected