From ad7899399bf4ec6f1dfcaf195bba99160908ccc7 Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Wed, 21 Oct 2026 13:23:06 +0000 Subject: [PATCH] fix(routes): read Go 1.22 ServeMux method and host patterns Since Go 1.22 a net/http ServeMux pattern may name a method and a host before the path: mux.HandleFunc("POST /v1/payments", h), "GET /v1/payments/{id}", "api.example.com/v1/x". Every route registration gate wanted the first argument to open with a slash (the extractor's handler capture, find_route_path_in_args in the parallel pass, and the three gates in the sequential pass), so such a registration minted no Route and no HANDLES edge, while the same file written with "/v1/payments" had both. cbm_service_pattern_mux_split reads the "[METHOD ][HOST]/[PATH]" grammar. For Go files only, the path of such a pattern now registers the route, the Route takes the pattern's method when the callee names none (Handle/HandleFunc are ANY), and a host is dropped from the path. Both passes share cbm_pipeline_mux_route_path and cbm_pipeline_route_reg_method so their Routes stay identical. Other languages and plain "/path" patterns take the unchanged path. Fixes #2537 Signed-off-by: Gaurav Dubey --- internal/cbm/extract_calls.c | 9 ++- internal/cbm/service_patterns.c | 44 +++++++++++ internal/cbm/service_patterns.h | 11 +++ src/pipeline/pass_calls.c | 30 ++++++-- src/pipeline/pass_parallel.c | 37 ++++++++- src/pipeline/pipeline_internal.h | 15 ++++ tests/test_extraction.c | 35 +++++++++ tests/test_infrascan.c | 34 +++++++++ tests/test_pipeline.c | 125 +++++++++++++++++++++++++++++++ 9 files changed, 327 insertions(+), 13 deletions(-) diff --git a/internal/cbm/extract_calls.c b/internal/cbm/extract_calls.c index 4eb02659c..f16b8ecf8 100644 --- a/internal/cbm/extract_calls.c +++ b/internal/cbm/extract_calls.c @@ -4553,8 +4553,13 @@ 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_split(call.first_string_arg, NULL, 0) != NULL)) && !(laravel_uri && php_route_action_is_string(node))) { call.second_arg_name = extract_handler_arg(ctx, args); } diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 7d428b8e3..1eaa50941 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -1183,6 +1183,50 @@ 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_broker(const char *resolved_qn) { if (!resolved_qn) { return NULL; diff --git a/internal/cbm/service_patterns.h b/internal/cbm/service_patterns.h index f06c85571..59f3ea0af 100644 --- a/internal/cbm/service_patterns.h +++ b/internal/cbm/service_patterns.h @@ -84,6 +84,17 @@ 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); + /* 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); diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index f3570904d..0e7069ab6 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -571,6 +571,21 @@ 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). 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(arg, 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 "". */ @@ -578,10 +593,11 @@ static void handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca 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", @@ -815,7 +831,7 @@ static void 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) { handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals, imp_count, route_mount, lang); return; @@ -1042,8 +1058,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM * apps. Classify by callee suffix + path-shaped first arg, exactly * like the parallel path's callee_suffix fallback; without this the * sequential path minted zero Route nodes for such files. */ - 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) { handle_route_registration(ctx, call, source_node, module_qn, imp_keys, imp_vals, imp_count, route_mount, lang); return SKIP_ONE; @@ -1220,8 +1236,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM * unresolved-callee fallback above mints, and nothing else. Dropping * the whole call lost every GET registration in a mixed-language repo * while POST (no `post` to collide with) survived. */ - 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) { handle_route_registration(ctx, call, source_node, module_qn, imp_keys, imp_vals, imp_count, route_mount, lang); return SKIP_ONE; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 73ce742ad..380110295 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2263,13 +2263,41 @@ 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 char *arg, CBMLanguage lang) { + if (lang != CBM_LANG_GO || !arg || cbm_service_pattern_is_comment_text(arg)) { + return NULL; + } + return cbm_service_pattern_mux_split(arg, NULL, 0); +} + +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->first_string_arg, 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). + * MUST match pass_calls.c route_reg_path. */ + const char *mux_path = cbm_pipeline_mux_route_path(call->first_string_arg, 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++) { @@ -2428,7 +2456,8 @@ static void 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]; @@ -2813,7 +2842,7 @@ static void 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) { emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn, registry, main_gbuf, imp_keys, imp_vals, imp_count, route_mount, @@ -2876,7 +2905,7 @@ static void emit_xlang_refused_route(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *so return; } 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) { emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn, registry, main_gbuf, imp_keys, imp_vals, imp_count, route_mount, lang); diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index fb5b724d6..d6882067e 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -787,6 +787,21 @@ 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 file, the path + * inside such a route-registration argument; NULL for any other language 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 char *arg, 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 diff --git a/tests/test_extraction.c b/tests/test_extraction.c index e1c1e4491..e94aa73c4 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -6207,6 +6207,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"` @@ -13442,6 +13476,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); diff --git a/tests/test_infrascan.c b/tests/test_infrascan.c index abfa8788c..2e33e5168 100644 --- a/tests/test_infrascan.c +++ b/tests/test_infrascan.c @@ -549,12 +549,46 @@ 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. */ +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, ""); + 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_data_flows_link_every_caller_of_a_route); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index bd76775f3..e30d95a26 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -13376,6 +13376,130 @@ TEST(pipeline_route_prefix_bridge_is_order_free_and_root_bridges_nothing) { PASS(); } +/* Issue #2537: since Go 1.22 a ServeMux pattern may name a method and a host + * before the path ("POST /v1/payments", "GET api.example.com/v1/x"). Such a + * registration minted no Route and no HANDLES edge, because every registration + * gate wanted the first argument to open with a slash, while the same file + * written with "/v1/payments" had both. The method now comes from the pattern + * and a host is dropped from the path, in the sequential and the parallel pass + * alike. Only Go reads that grammar: a Python registration of "GET /py" is still + * no route. */ +TEST(pipeline_go_servemux_method_patterns_issue2537) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_servemux_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_temp_file(tmp, "go.mod", "module example.com/srv\n\ngo 1.22\n"); + write_temp_file(tmp, "main.go", + "package main\n" + "\n" + "import \"net/http\"\n" + "\n" + "func createPayment(w http.ResponseWriter, r *http.Request) {}\n" + "func getPayment(w http.ResponseWriter, r *http.Request) {}\n" + "func hostOnly(w http.ResponseWriter, r *http.Request) {}\n" + "func legacyList(w http.ResponseWriter, r *http.Request) {}\n" + "\n" + "func main() {\n" + "\tmux := http.NewServeMux()\n" + "\tmux.HandleFunc(\"POST /v1/payments\", createPayment)\n" + "\tmux.HandleFunc(\"GET /v1/payments/{id}\", getPayment)\n" + "\tmux.HandleFunc(\"api.example.com/v1/hosted\", hostOnly)\n" + "\tmux.HandleFunc(\"/v1/legacy\", legacyList)\n" + "\t_ = http.ListenAndServe(\":8080\", mux)\n" + "}\n"); + write_temp_file(tmp, "web/site.py", + "from flask import Flask\n" + "app = Flask(__name__)\n" + "\n" + "def py_view():\n" + " return \"ok\"\n" + "\n" + "app.add_url_rule(\"GET /py\", \"py_view\", py_view)\n"); + /* past MIN_FILES_FOR_PARALLEL, so the threaded run takes the parallel pass */ + for (int i = 0; i < 52; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "pad/padding%d.go", i); + snprintf(body, sizeof(body), "package pad\n\nfunc Padding%d() int { return %d }\n", i, i); + write_temp_file(tmp, name, body); + } + const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD"); + char *saved_single = old_single ? strdup(old_single) : NULL; + const char *old_workers = getenv("CBM_WORKERS"); + char *saved_workers = old_workers ? strdup(old_workers) : NULL; + int run_rc[2] = {-1, -1}; + bool opened[2] = {false, false}; + int post[2] = {-1, -1}; + int get_id[2] = {-1, -1}; + int any_post[2] = {-1, -1}; + int host[2] = {-1, -1}; + int legacy[2] = {-1, -1}; + int py_any[2] = {-1, -1}; + int py_get[2] = {-1, -1}; + for (int mode = 0; mode < 2; mode++) { + if (mode == 0) { + cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1); + } else { + cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); + cbm_setenv("CBM_WORKERS", "4", 1); + } + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/servemux_%d.db", tmp, mode); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + run_rc[mode] = p ? cbm_pipeline_run(p) : -1; + const char *project = p ? cbm_pipeline_project_name(p) : NULL; + cbm_store_t *s = cbm_store_open_path(db_path); + opened[mode] = s != NULL; + if (s && project) { + post[mode] = + handles_to_route(s, project, "createPayment", "__route__POST__/v1/payments"); + get_id[mode] = + handles_to_route(s, project, "getPayment", "__route__GET__/v1/payments/{}"); + any_post[mode] = + handles_to_route(s, project, "createPayment", "__route__ANY__/v1/payments"); + host[mode] = handles_to_route(s, project, "hostOnly", "__route__ANY__/v1/hosted"); + legacy[mode] = handles_to_route(s, project, "legacyList", "__route__ANY__/v1/legacy"); + py_any[mode] = handles_to_route(s, project, "py_view", "__route__ANY__/py"); + py_get[mode] = handles_to_route(s, project, "py_view", "__route__GET__/py"); + } + if (s) { + cbm_store_close(s); + } + cbm_pipeline_free(p); + } + if (saved_single) { + cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1); + free(saved_single); + } else { + cbm_unsetenv("CBM_INDEX_SINGLE_THREAD"); + } + if (saved_workers) { + cbm_setenv("CBM_WORKERS", saved_workers, 1); + free(saved_workers); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + th_rmtree(tmp); + for (int mode = 0; mode < 2; mode++) { + ASSERT_EQ(run_rc[mode], 0); + ASSERT_TRUE(opened[mode]); + /* the method is the pattern's, not ANY */ + ASSERT_EQ(post[mode], 1); + ASSERT_EQ(get_id[mode], 1); + ASSERT_EQ(any_post[mode], 0); + /* a host names no method and is not part of the path */ + ASSERT_EQ(host[mode], 1); + /* control: a plain path is unchanged */ + ASSERT_EQ(legacy[mode], 1); + /* another language's "GET /py" is not read as a ServeMux pattern */ + ASSERT_EQ(py_any[mode], 0); + ASSERT_EQ(py_get[mode], 0); + } + PASS(); +} + TEST(pipeline_swift_http_call_makes_route_issue1892) { char tmp[256]; snprintf(tmp, sizeof(tmp), "/tmp/cbm_swifthttp_XXXXXX"); @@ -27725,6 +27849,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_swift_nested_url_makes_route_issue1892); RUN_TEST(pipeline_swift_http_call_makes_route_issue1892); RUN_TEST(pipeline_route_prefix_bridge_is_order_free_and_root_bridges_nothing); + RUN_TEST(pipeline_go_servemux_method_patterns_issue2537); RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls); RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http); RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin);