Attach source or context lines to a search result JSON item. */
| 8799 | |
| 8800 | /* Attach source or context lines to a search result JSON item. */ |
| 8801 | static void attach_result_source(yyjson_mut_doc *doc, yyjson_mut_val *item, search_result_t *r, |
| 8802 | int mode, int context_lines, const char *root_path) { |
| 8803 | enum { MODE_FULL = 1 }; |
| 8804 | if (r->start_line <= 0 || r->end_line <= 0) { |
| 8805 | return; |
| 8806 | } |
| 8807 | char abs_path[CBM_SZ_1K]; |
| 8808 | snprintf(abs_path, sizeof(abs_path), "%s/%s", root_path, r->file); |
| 8809 | |
| 8810 | /* Containment: a search result whose indexed path resolves outside the |
| 8811 | * project root (a `..` segment, or a symlink/junction that discovery |
| 8812 | * followed) must not be read back into the response. Same guard the |
| 8813 | * snippet path already uses. */ |
| 8814 | if (!cbm_path_within_root(root_path, abs_path)) { |
| 8815 | return; |
| 8816 | } |
| 8817 | |
| 8818 | if (mode == MODE_FULL) { |
| 8819 | /* Cap each hit's source at a match-anchored window: uncapped |
| 8820 | * whole-symbol dumps ran to 5.7KB × N hits (142KB responses). The |
| 8821 | * complete symbol stays one get_code_snippet call away; |
| 8822 | * source_start/source_truncated make the cut explicit. */ |
| 8823 | enum { SC_FULL_MAX_LINES = 60, SC_FULL_LEAD = 5 }; |
| 8824 | int s = r->start_line; |
| 8825 | int e = r->end_line; |
| 8826 | bool truncated = false; |
| 8827 | if (e - s + 1 > SC_FULL_MAX_LINES) { |
| 8828 | if (r->match_count > 0 && r->match_lines[0] - SC_FULL_LEAD > s) { |
| 8829 | s = r->match_lines[0] - SC_FULL_LEAD; |
| 8830 | } |
| 8831 | e = s + SC_FULL_MAX_LINES - 1; |
| 8832 | if (e > r->end_line) { |
| 8833 | e = r->end_line; |
| 8834 | } |
| 8835 | truncated = true; |
| 8836 | } |
| 8837 | char *source = read_file_lines(abs_path, s, e); |
| 8838 | if (source) { |
| 8839 | sanitize_ascii(source); |
| 8840 | yyjson_mut_obj_add_strcpy(doc, item, "source", source); |
| 8841 | free(source); |
| 8842 | if (truncated) { |
| 8843 | yyjson_mut_obj_add_int(doc, item, "source_start", s); |
| 8844 | yyjson_mut_obj_add_bool(doc, item, "source_truncated", true); |
| 8845 | } |
| 8846 | } |
| 8847 | } else if (context_lines > 0 && r->match_count > 0) { |
| 8848 | int ctx_start = r->match_lines[0] - context_lines; |
| 8849 | int ctx_end = r->match_lines[r->match_count - SKIP_ONE] + context_lines; |
| 8850 | if (ctx_start < SKIP_ONE) { |
| 8851 | ctx_start = SKIP_ONE; |
| 8852 | } |
| 8853 | char *ctx = read_file_lines(abs_path, ctx_start, ctx_end); |
| 8854 | if (ctx) { |
| 8855 | sanitize_ascii(ctx); |
| 8856 | yyjson_mut_obj_add_strcpy(doc, item, "context", ctx); |
| 8857 | yyjson_mut_obj_add_int(doc, item, "context_start", ctx_start); |
| 8858 | free(ctx); |
no test coverage detected