Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -4557,8 +4557,14 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
call.first_string_arg = route_path_with_group_prefix(ctx, node, call.callee_name,
call.first_string_arg);
/* A slashless registration's string action would only be
* guessed at (#1146): mint its Route without a handler. */
if (call.first_string_arg && call.first_string_arg[0] == '/' &&
* guessed at (#1146): mint its Route without a handler. A Go
* ServeMux pattern names its method or host before the slash
* ("POST /v1/x", #2537) and keeps its handler. */
if (call.first_string_arg &&
(call.first_string_arg[0] == '/' ||
(ctx->language == CBM_LANG_GO &&
cbm_service_pattern_mux_route_path(call.callee_name, call.first_string_arg) !=
NULL)) &&
!(laravel_uri && php_route_action_is_string(node))) {
call.second_arg_name = extract_handler_arg(ctx, args);
}
Expand Down
59 changes: 59 additions & 0 deletions internal/cbm/service_patterns.c
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,65 @@ const char *cbm_service_pattern_route_method(const char *callee_name) {
return NULL;
}

static bool mux_is_blank(char ch) {
return ch == ' ' || ch == '\t';
}

const char *cbm_service_pattern_mux_split(const char *pattern, char *method, size_t method_sz) {
if (method && method_sz > 0) {
method[0] = '\0';
}
if (!pattern || pattern[0] == '/') {
return NULL;
}
/* METHOD: Go cuts it at the first space or tab and trims the rest. */
const char *rest = pattern;
size_t method_len = 0;
while (pattern[method_len] >= 'A' && pattern[method_len] <= 'Z') {
method_len++;
}
if (method_len > 0 && mux_is_blank(pattern[method_len])) {
rest = pattern + method_len;
while (mux_is_blank(*rest)) {
rest++;
}
} else {
method_len = 0;
}
/* HOST: everything before the path's slash, with no blank or line break. */
const char *slash = rest;
while (*slash && *slash != '/' && !mux_is_blank(*slash) && *slash != '\n' && *slash != '\r') {
slash++;
}
if (*slash != '/' || (method_len == 0 && slash == rest)) {
return NULL;
}
if (method_len > 0 && method) {
if (method_len >= method_sz) {
method[0] = '\0';
return NULL;
}
memcpy(method, pattern, method_len);
method[method_len] = '\0';
}
return slash;
}

const char *cbm_service_pattern_mux_route_path(const char *callee_name, const char *pattern) {
static const char *const mux_callees[] = {".Handle", ".HandleFunc"};
if (!callee_name || !pattern || strstr(pattern, "://") != NULL) {
return NULL;
}
size_t clen = strlen(callee_name);
for (size_t i = 0; i < sizeof(mux_callees) / sizeof(mux_callees[0]); i++) {
size_t slen = strlen(mux_callees[i]);
if (clen >= slen && strcmp(callee_name + clen - slen, mux_callees[i]) == 0) {
return cbm_service_pattern_mux_split(pattern, NULL, 0);
}
}
return NULL;
}

const char *cbm_service_pattern_broker(const char *resolved_qn) {
if (!resolved_qn) {
return NULL;
Expand Down
19 changes: 19 additions & 0 deletions internal/cbm/service_patterns.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ const char *cbm_service_pattern_http_method(const char *callee_name);
* Returns NULL if not a known route registration method. */
const char *cbm_service_pattern_route_method(const char *callee_name);

/* Go 1.22+ net/http ServeMux pattern "[METHOD ][HOST]/[PATH]" (#2537):
* "POST /v1/x", "GET api.example.com/v1/x", "api.example.com/v1/x". Returns a
* pointer to PATH's leading slash inside `pattern`, or NULL when the pattern
* names neither a method nor a host before a slash (a plain "/v1/x" is NULL
* too: there is nothing to split). METHOD is a run of uppercase letters ended
* by spaces or tabs; it is copied to `method` (NUL-terminated, "" when the
* pattern names none) when `method` is non-NULL; a method that does not fit
* `method_sz` then makes the pattern NULL. The grammar is Go's: callers gate
* on the file language. */
const char *cbm_service_pattern_mux_split(const char *pattern, char *method, size_t method_sz);

/* The path of a ServeMux registration's pattern (#2537): only a callee ending
* ".Handle" or ".HandleFunc" registers one, and never with a URL ("://").
* Another route-verb call names no pattern: `store.Get("users/123")` is no
* host "users" + path "/123", `client.Get("https://...")` no host "https:".
* The split of cbm_service_pattern_mux_split for such a registration, else
* NULL. The grammar is Go's: callers gate on the file language. */
const char *cbm_service_pattern_mux_route_path(const char *callee_name, const char *pattern);

/* Get the broker name for an async QN (e.g., "pubsub" from a Pub/Sub QN).
* Returns NULL if not an async pattern. */
const char *cbm_service_pattern_broker(const char *resolved_qn);
Expand Down
27 changes: 22 additions & 5 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -577,17 +577,34 @@ cbm_svc_kind_t cbm_pipeline_external_import_service(cbm_external_import_index_t
return CBM_SVC_NONE;
}

/* The path a route registration's first argument names: the argument itself
* when it opens with a slash, or in Go the path of a ServeMux pattern that
* names a method or host first ("POST /v1/x", #2537; Handle / HandleFunc
* only). NULL when it names no path. MUST match pass_parallel.c
* find_route_path_in_args. */
static const char *route_reg_path(const CBMCall *call, CBMLanguage lang) {
const char *arg = call->first_string_arg;
if (!arg) {
return NULL;
}
if (arg[0] == '/') {
return arg;
}
return cbm_pipeline_mux_route_path(call, lang);
}

/* Handle a route registration call: create Route node + HANDLES edge.
* route_mount is the framework mount of the registering file ("/api" for a
* Laravel 11+ `withRouting(api: ...)` file, #1146) or "". */
static bool handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
const cbm_gbuf_node_t *source_node, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count,
const char *route_mount, CBMLanguage lang) {
const char *method = cbm_service_pattern_route_method(call->callee_name);
char mux_method[CBM_SZ_16];
const char *method = cbm_pipeline_route_reg_method(call, lang, mux_method, sizeof(mux_method));
char mounted[CBM_SZ_256];
const char *path =
cbm_laravel_mount_route(route_mount, call->first_string_arg, mounted, sizeof(mounted));
cbm_laravel_mount_route(route_mount, route_reg_path(call, lang), mounted, sizeof(mounted));
char route_qn[CBM_ROUTE_QN_SIZE];
char cpath[CBM_SZ_256];
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method ? method : "ANY",
Expand Down Expand Up @@ -824,7 +841,7 @@ static bool emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
bool suppress_plain_calls, const char *route_mount,
CBMLanguage lang) {
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
if (svc == CBM_SVC_ROUTE_REG && call->first_string_arg && call->first_string_arg[0] == '/') {
if (svc == CBM_SVC_ROUTE_REG && route_reg_path(call, lang) != NULL) {
return handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals,
imp_count, route_mount, lang);
}
Expand Down Expand Up @@ -924,8 +941,8 @@ static bool calls_emit_route_call(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
const char **imp_keys, const char **imp_vals, int imp_count,
const char *route_mount, CBMLanguage lang,
CBMCallEvidence *evidence) {
if (cbm_service_pattern_route_method(call->callee_name) == NULL || !call->first_string_arg ||
call->first_string_arg[0] != '/') {
if (cbm_service_pattern_route_method(call->callee_name) == NULL ||
route_reg_path(call, lang) == NULL) {
return false;
}
evidence->calls_emitted |= handle_route_registration(
Expand Down
38 changes: 34 additions & 4 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2272,13 +2272,42 @@ static bool is_route_path_shaped(const char *val) {
return val && val[0] == '/' && !cbm_service_pattern_is_comment_text(val);
}

static const char *find_route_path_in_args(const CBMCall *call, const char **out_handler) {
const char *cbm_pipeline_mux_route_path(const CBMCall *call, CBMLanguage lang) {
const char *arg = call->first_string_arg;
if (lang != CBM_LANG_GO || !arg || cbm_service_pattern_is_comment_text(arg)) {
return NULL;
}
return cbm_service_pattern_mux_route_path(call->callee_name, arg);
}

const char *cbm_pipeline_route_reg_method(const CBMCall *call, CBMLanguage lang, char *buf,
size_t buf_sz) {
const char *method = cbm_service_pattern_route_method(call->callee_name);
if (method && strcmp(method, "ANY") != 0) {
return method;
}
if (cbm_pipeline_mux_route_path(call, lang) &&
cbm_service_pattern_mux_split(call->first_string_arg, buf, buf_sz) && buf[0] != '\0') {
return buf;
}
return method;
}

static const char *find_route_path_in_args(const CBMCall *call, const char **out_handler,
CBMLanguage lang) {
*out_handler = NULL;
/* 1. First string arg starting with / */
if (is_route_path_shaped(call->first_string_arg)) {
*out_handler = call->second_arg_name;
return call->first_string_arg;
}
/* 1b. Go ServeMux pattern with a method or host before the path (#2537),
* read for Handle / HandleFunc only. MUST match pass_calls.c route_reg_path. */
const char *mux_path = cbm_pipeline_mux_route_path(call, lang);
if (mux_path) {
*out_handler = call->second_arg_name;
return mux_path;
}
/* 2. Keyword args (prefix=, path=, route=, etc.) */
const char *found = NULL;
for (int ai = 0; ai < call->arg_count && !found; ai++) {
Expand Down Expand Up @@ -2438,7 +2467,8 @@ static bool emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *sou
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
const char **ik, const char **iv, int ic,
const char *route_mount, CBMLanguage lang) {
const char *method = cbm_service_pattern_route_method(call->callee_name);
char mux_method[CBM_SZ_16];
const char *method = cbm_pipeline_route_reg_method(call, lang, mux_method, sizeof(mux_method));
char mounted[CBM_SZ_256];
route_path = cbm_laravel_mount_route(route_mount, route_path, mounted, sizeof(mounted));
char rqn[CBM_ROUTE_QN_SIZE];
Expand Down Expand Up @@ -2824,7 +2854,7 @@ static bool emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,

if (svc == CBM_SVC_ROUTE_REG) {
const char *handler_ref = NULL;
const char *route_path = find_route_path_in_args(call, &handler_ref);
const char *route_path = find_route_path_in_args(call, &handler_ref, lang);
if (route_path) {
return emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn,
registry, main_gbuf, imp_keys, imp_vals, imp_count,
Expand Down Expand Up @@ -2888,7 +2918,7 @@ static bool emit_xlang_refused_route(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *so
return false;
}
const char *handler_ref = NULL;
const char *route_path = find_route_path_in_args(call, &handler_ref);
const char *route_path = find_route_path_in_args(call, &handler_ref, lang);
if (route_path) {
return emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn,
registry, main_gbuf, imp_keys, imp_vals, imp_count,
Expand Down
16 changes: 16 additions & 0 deletions src/pipeline/pipeline_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,22 @@ bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *proje
* behind both passes' HTTP_CALLS / ASYNC_CALLS props (pass_parallel.c). */
void cbm_pipeline_close_call_props(char *props, int n, const CBMCall *call, bool with_line);

/* Go 1.22 ServeMux registrations may name a method and a host before the path
* ("POST /v1/x", "GET api.example.com/v1/x", #2537). For a Go `Handle` /
* `HandleFunc` call, the path inside its first argument; NULL for any other
* language or callee (`store.Get("users/123")`), for a URL argument, and for an
* argument that is no such pattern (a plain "/v1/x" included). The one reader
* of that grammar behind both passes (pass_parallel.c). */
const char *cbm_pipeline_mux_route_path(const CBMCall *call, CBMLanguage lang);

/* The method of a route registration's Route: the verb its callee names
* (`router.GET`), else the method its Go ServeMux pattern names
* (`mux.HandleFunc("POST /v1/x", h)`, #2537), else the callee's own answer
* (ANY or NULL). `buf` (CBM_SZ_16) holds a pattern method. Shared by both
* passes so their Routes stay byte-identical (pass_parallel.c). */
const char *cbm_pipeline_route_reg_method(const CBMCall *call, CBMLanguage lang, char *buf,
size_t buf_sz);

/* Per-file index of the names a file's imports bind onto EXTERNAL modules:
* local name -> the binding import. Built lazily, on the first call that
* asks, from the file's extraction imports and its IMPORTS edges; one hash
Expand Down
35 changes: 35 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -6431,6 +6431,40 @@ TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249) {
PASS();
}

/* Issue #2537: a Go 1.22 ServeMux pattern may open with a method and/or a host
* ("POST /v1/payments", "GET api.example.com/v1/x"). The written pattern stays
* the first string argument, and its handler is captured exactly as for a
* plain "/v1/payments"; it was dropped because the pattern does not open with
* a slash. */
TEST(extract_go_servemux_method_pattern_handler_issue2537) {
CBMFileResult *r = extract("package main\n"
"import \"net/http\"\n"
"func setup(mux *http.ServeMux) {\n"
" mux.HandleFunc(\"POST /v1/payments\", createPayment)\n"
" mux.Handle(\"GET api.example.com/v1/x\", hostHandler)\n"
"}\n",
CBM_LANG_GO, "t", "routes.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);

const CBMCall *method = find_call_by_callee(r, "mux.HandleFunc");
ASSERT_NOT_NULL(method);
ASSERT_NOT_NULL(method->first_string_arg);
ASSERT_STR_EQ(method->first_string_arg, "POST /v1/payments");
ASSERT_NOT_NULL(method->second_arg_name);
ASSERT_STR_EQ(method->second_arg_name, "createPayment");

const CBMCall *host = find_call_by_callee(r, "mux.Handle");
ASSERT_NOT_NULL(host);
ASSERT_NOT_NULL(host->first_string_arg);
ASSERT_STR_EQ(host->first_string_arg, "GET api.example.com/v1/x");
ASSERT_NOT_NULL(host->second_arg_name);
ASSERT_STR_EQ(host->second_arg_name, "hostHandler");

cbm_free_result(r);
PASS();
}

/* Issue #2291: the JS/TS twin of #1249. A BFF that builds its backend URL as
* `${process.env.BACKEND_URL}/wallet/topup` from a configurable base must
* index the literal path after the base, exactly as `BACKEND_URL + "/x"`
Expand Down Expand Up @@ -13907,6 +13941,7 @@ SUITE(extraction) {
RUN_TEST(extract_ts_config_object_url_issue2235);
RUN_TEST(extract_go_binary_concat_url_issue1249);
RUN_TEST(extract_go_binary_concat_url_no_literal_suffix_issue1249);
RUN_TEST(extract_go_servemux_method_pattern_handler_issue2537);
RUN_TEST(extract_ts_template_base_url_issue2291);
RUN_TEST(extract_ts_template_base_url_no_literal_path_issue2291);
RUN_TEST(extract_py_concat_const_url_issue706);
Expand Down
49 changes: 49 additions & 0 deletions tests/test_infrascan.c
Original file line number Diff line number Diff line change
Expand Up @@ -809,12 +809,61 @@ TEST(infrascan_data_flows_test_handlers_pair_only_within_their_file) {
PASS();
}

/* Issue #2537: the Go 1.22 ServeMux grammar "[METHOD ][HOST]/[PATH]". The path
* is found inside the written pattern; a plain path, a lowercase word, a
* pattern without a path and a method too long for the buffer are no split.
* Only a Handle / HandleFunc registration is read with it. */
TEST(infrascan_servemux_pattern_split_issue2537) {
char m[16];
const char *pat = "POST /v1/payments";
ASSERT_TRUE(cbm_service_pattern_mux_split(pat, m, sizeof(m)) == pat + 5);
ASSERT_STR_EQ(m, "POST");
pat = "GET \t /v1/payments/{id}";
ASSERT_TRUE(cbm_service_pattern_mux_split(pat, m, sizeof(m)) == pat + 6);
ASSERT_STR_EQ(m, "GET");
pat = "GET api.example.com/v1/x";
ASSERT_TRUE(cbm_service_pattern_mux_split(pat, m, sizeof(m)) == pat + 19);
ASSERT_STR_EQ(m, "GET");
pat = "api.example.com:8443/v1/x";
ASSERT_TRUE(cbm_service_pattern_mux_split(pat, m, sizeof(m)) == pat + 20);
ASSERT_STR_EQ(m, "");
pat = "DELETE /";
ASSERT_TRUE(cbm_service_pattern_mux_split(pat, NULL, 0) == pat + 7);

ASSERT_NULL(cbm_service_pattern_mux_split("/v1/x", m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split("get /v1/x", m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split("POST", m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split("POST ", m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split("GET host", m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split("GET a b/x", m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split("", m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split(NULL, m, sizeof(m)));
ASSERT_NULL(cbm_service_pattern_mux_split("ABCDEFGHIJKLMNOP /x", m, sizeof(m)));
ASSERT_STR_EQ(m, "");

/* only Handle / HandleFunc register a pattern, and never with a URL */
pat = "POST /v1/payments";
ASSERT_TRUE(cbm_service_pattern_mux_route_path("mux.HandleFunc", pat) == pat + 5);
pat = "api.example.com/v1/x";
ASSERT_TRUE(cbm_service_pattern_mux_route_path("s.mux.Handle", pat) == pat + 15);
ASSERT_NULL(cbm_service_pattern_mux_route_path("st.Get", "users/123"));
ASSERT_NULL(cbm_service_pattern_mux_route_path("c.Get", "https://api.example.com/v1/x"));
ASSERT_NULL(cbm_service_pattern_mux_route_path("mux.Handle", "https://api.example.com/v1/x"));
ASSERT_NULL(cbm_service_pattern_mux_route_path("PatchCollector.Delete", "storage.k8s.io/v1"));
ASSERT_NULL(cbm_service_pattern_mux_route_path("r.Route", "GET /v1/x"));
ASSERT_NULL(cbm_service_pattern_mux_route_path("mux.HandleFunc", "/v1/x"));
ASSERT_NULL(cbm_service_pattern_mux_route_path(NULL, "GET /v1/x"));
ASSERT_NULL(cbm_service_pattern_mux_route_path("mux.HandleFunc", NULL));
PASS();
}

SUITE(infrascan) {
RUN_TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths);
RUN_TEST(infrascan_http_route_literal_guard_rejects_document_extensions);
RUN_TEST(infrascan_client_navigation_is_js_family_only);
RUN_TEST(infrascan_service_pattern_match_rejects_ids_inside_words);
RUN_TEST(infrascan_service_pattern_match_keeps_real_library_qns);
RUN_TEST(infrascan_servemux_pattern_split_issue2537);
RUN_TEST(infrascan_route_nodes_skip_bad_http_url_paths);
RUN_TEST(infrascan_http_calls_join_matching_handler_route);
RUN_TEST(infrascan_decorator_route_metadata_keeps_its_escapes);
Expand Down
Loading
Loading