Attach source or context lines to a search result JSON item. */
| 9042 | |
| 9043 | /* Attach source or context lines to a search result JSON item. */ |
| 9044 | static void attach_result_source(yyjson_mut_doc *doc, yyjson_mut_val *item, search_result_t *r, |
| 9045 | int mode, int context_lines, const char *root_path) { |
| 9046 | enum { MODE_FULL = 1 }; |
| 9047 | if (r->start_line <= 0 || r->end_line <= 0) { |
| 9048 | return; |
| 9049 | } |
| 9050 | char abs_path[CBM_SZ_1K]; |
| 9051 | snprintf(abs_path, sizeof(abs_path), "%s/%s", root_path, r->file); |
| 9052 | |
| 9053 | /* Containment: a search result whose indexed path resolves outside the |
| 9054 | * project root (a `..` segment, or a symlink/junction that discovery |
| 9055 | * followed) must not be read back into the response. Same guard the |
| 9056 | * snippet path already uses. */ |
| 9057 | if (!cbm_path_within_root(root_path, abs_path)) { |
| 9058 | return; |
| 9059 | } |
| 9060 | |
| 9061 | if (mode == MODE_FULL) { |
| 9062 | /* Cap each hit's source at a match-anchored window: uncapped |
| 9063 | * whole-symbol dumps ran to 5.7KB × N hits (142KB responses). The |
| 9064 | * complete symbol stays one get_code_snippet call away; |
| 9065 | * source_start/source_truncated make the cut explicit. */ |
| 9066 | enum { SC_FULL_MAX_LINES = 60, SC_FULL_LEAD = 5 }; |
| 9067 | int s = r->start_line; |
| 9068 | int e = r->end_line; |
| 9069 | bool truncated = false; |
| 9070 | if (e - s + 1 > SC_FULL_MAX_LINES) { |
| 9071 | if (r->match_count > 0 && r->match_lines[0] - SC_FULL_LEAD > s) { |
| 9072 | s = r->match_lines[0] - SC_FULL_LEAD; |
| 9073 | } |
| 9074 | e = s + SC_FULL_MAX_LINES - 1; |
| 9075 | if (e > r->end_line) { |
| 9076 | e = r->end_line; |
| 9077 | } |
| 9078 | truncated = true; |
| 9079 | } |
| 9080 | char *source = read_file_lines(abs_path, s, e); |
| 9081 | if (source) { |
| 9082 | char *safe_source = sanitize_utf8_lossy(source); |
| 9083 | if (safe_source) { |
| 9084 | yyjson_mut_obj_add_strcpy(doc, item, "source", safe_source); |
| 9085 | free(safe_source); |
| 9086 | } |
| 9087 | free(source); |
| 9088 | if (truncated) { |
| 9089 | yyjson_mut_obj_add_int(doc, item, "source_start", s); |
| 9090 | yyjson_mut_obj_add_bool(doc, item, "source_truncated", true); |
| 9091 | } |
| 9092 | } |
| 9093 | } else if (context_lines > 0 && r->match_count > 0) { |
| 9094 | int ctx_start = r->match_lines[0] - context_lines; |
| 9095 | int ctx_end = r->match_lines[r->match_count - SKIP_ONE] + context_lines; |
| 9096 | if (ctx_start < SKIP_ONE) { |
| 9097 | ctx_start = SKIP_ONE; |
| 9098 | } |
| 9099 | char *ctx = read_file_lines(abs_path, ctx_start, ctx_end); |
| 9100 | if (ctx) { |
| 9101 | char *safe_context = sanitize_utf8_lossy(ctx); |
no test coverage detected