diff --git a/README.md b/README.md index d87336f..1b7a74a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This generator, from a webrpc schema/design file, will code-generate: 2. Implementation output C JSON encode/decode helpers, generated method request / response handling, - and an optional `libcurl`-based client runtime. + and a self-contained `libcurl`-based HTTP transport/runtime. The generated client is intended to speak to any webrpc server language (Go, nodejs, etc.) as long as the schema features used are supported by this target. @@ -28,6 +28,14 @@ Generated `impl` output currently depends on: The generated code targets C99. +When using generated implementation output that sends requests through the +generated `libcurl` transport, call the generated runtime hooks before the +first request and after the last one. For example, if you generate with +`-prefix=example`, call `example_runtime_init()` before the first request and +`example_runtime_cleanup()` after the last one. This follows libcurl's +documented global initialization model: +[libcurl API overview](https://curl.se/libcurl/c/libcurl.html). + Typical compile / link flags look like: ```bash @@ -77,6 +85,7 @@ The current generator does not support: - streaming methods - map keys other than `string` or `enum` - a shared external transport abstraction; the generated runtime is currently self-contained +- automatic redirect following Implementation generation also assumes a companion generated header include via `-header=`. diff --git a/_examples/Makefile b/_examples/Makefile index 21584b3..ff05636 100644 --- a/_examples/Makefile +++ b/_examples/Makefile @@ -1,4 +1,4 @@ -WEBRPC_GEN_VERSION := v0.37.1 +WEBRPC_GEN_VERSION := v0.37.2 ROOT := .. GEN := go run -ldflags="-X github.com/webrpc/webrpc.VERSION=$(WEBRPC_GEN_VERSION)" github.com/webrpc/webrpc/cmd/webrpc-gen@$(WEBRPC_GEN_VERSION) diff --git a/_examples/smoke/example.gen.c b/_examples/smoke/example.gen.c index 0e736ec..b8ec801 100644 --- a/_examples/smoke/example.gen.c +++ b/_examples/smoke/example.gen.c @@ -1,6 +1,6 @@ // smoke v1.0.0 6dc82371b24d044c3b3e2bd4cf5d92af46788fff // -- -// Code generated by webrpc-gen@v0.37.1 with .. generator. DO NOT EDIT. +// Code generated by webrpc-gen@v0.37.2 with .. generator. DO NOT EDIT. // // webrpc-gen -schema=smoke/example.ridl -target=.. -emit=impl -header=example.gen.h -out=smoke/example.gen.c @@ -134,9 +134,16 @@ static void smoke_set_error( } static int smoke_buffer_grow(smoke_buffer *buf, size_t need) { + if (!buf) return 0; if (buf->cap >= need) return 1; size_t new_cap = buf->cap ? buf->cap : 1024; - while (new_cap < need) new_cap *= 2; + while (new_cap < need) { + if (new_cap > SIZE_MAX / 2) { + new_cap = need; + break; + } + new_cap *= 2; + } char *next = (char *)realloc(buf->data, new_cap); if (!next) return 0; buf->data = next; @@ -145,8 +152,13 @@ static int smoke_buffer_grow(smoke_buffer *buf, size_t need) { } static size_t smoke_write_cb(char *ptr, size_t size, size_t nmemb, void *userdata) { - size_t n = size * nmemb; smoke_buffer *buf = (smoke_buffer *)userdata; + size_t n; + + if (!buf) return 0; + if (size != 0 && nmemb > SIZE_MAX / size) return 0; + n = size * nmemb; + if (buf->len > SIZE_MAX - n - 1) return 0; if (!smoke_buffer_grow(buf, buf->len + n + 1)) return 0; memcpy(buf->data + buf->len, ptr, n); buf->len += n; @@ -248,66 +260,22 @@ static int smoke_prepared_request_overrides_header(const smoke_prepared_request return 0; } -static int smoke_curl_runtime_init(smoke_error *error) { - static int initialized = 0; - - if (initialized) { - return 0; - } - - if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) { - smoke_set_error(error, 0, 0, "TransportError", "curl_global_init failed", NULL); - return -1; - } - - if (atexit(curl_global_cleanup) != 0) { - curl_global_cleanup(); - smoke_set_error(error, 0, 0, "TransportError", "failed to register curl_global_cleanup", NULL); - return -1; - } - - initialized = 1; - return 0; -} - -static int smoke_http_send_request( - const char *base_url, +static int smoke_build_request_headers( const smoke_prepared_request *request, const char *bearer_token, const struct curl_slist *default_headers, - long timeout_ms, - smoke_http_response *response, + struct curl_slist **out_headers, smoke_error *error ) { - const char *http_method; - smoke_http_response result; int has_content_type = 0; int has_authorization = 0; - smoke_http_response_init(&result); - - if (!request || !response) { - smoke_set_error(error, 0, 0, "ClientError", "request and response must be non-NULL", NULL); - return -1; - } - - if (smoke_curl_runtime_init(error) != 0) { - return -1; - } - - CURL *curl = curl_easy_init(); - if (!curl) { - smoke_set_error(error, 0, 0, "TransportError", "curl_easy_init failed", NULL); - return -1; - } + struct curl_slist *headers = NULL; - char *url = smoke_join_url(base_url, request->path ? request->path : ""); - if (!url) { - smoke_set_error(error, 0, 0, "TransportError", "failed to build URL", NULL); - curl_easy_cleanup(curl); + if (!request || !out_headers) { + smoke_set_error(error, 0, 0, "ClientError", "request headers must be built from a valid request", NULL); return -1; } - struct curl_slist *headers = NULL; for (const struct curl_slist *it = default_headers; it; it = it->next) { if ((bearer_token && bearer_token[0] != '\0' && smoke_header_name_equals(it->data, "Authorization")) || smoke_prepared_request_overrides_header(request, it->data)) { @@ -316,8 +284,6 @@ static int smoke_http_send_request( if (!smoke_append_header(&headers, it->data)) { smoke_set_error(error, 0, 0, "TransportError", "failed to copy request headers", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if (smoke_header_name_equals(it->data, "Content-Type")) { @@ -334,8 +300,6 @@ static int smoke_http_send_request( if (!content_type_header) { smoke_set_error(error, 0, 0, "TransportError", "failed to allocate content-type header", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } snprintf(content_type_header, need, "Content-Type: %s", request->content_type); @@ -343,8 +307,6 @@ static int smoke_http_send_request( smoke_set_error(error, 0, 0, "TransportError", "failed to append content-type header", NULL); free(content_type_header); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } free(content_type_header); @@ -355,15 +317,11 @@ static int smoke_http_send_request( if (!request->headers || !request->headers[i]) { smoke_set_error(error, 0, 0, "TransportError", "request headers are invalid", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if (!smoke_append_header(&headers, request->headers[i])) { smoke_set_error(error, 0, 0, "TransportError", "failed to append request header", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if (smoke_header_name_equals(request->headers[i], "Content-Type")) { @@ -377,8 +335,6 @@ static int smoke_http_send_request( if (!has_content_type && !smoke_append_header(&headers, "Content-Type: application/json")) { smoke_set_error(error, 0, 0, "TransportError", "failed to allocate request headers", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if (bearer_token && bearer_token[0] != '\0' && !has_authorization) { @@ -387,8 +343,6 @@ static int smoke_http_send_request( if (!auth_header) { smoke_set_error(error, 0, 0, "TransportError", "failed to allocate auth header", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } snprintf(auth_header, need, "Authorization: Bearer %s", bearer_token); @@ -396,49 +350,113 @@ static int smoke_http_send_request( smoke_set_error(error, 0, 0, "TransportError", "failed to append auth header", NULL); free(auth_header); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } free(auth_header); } + *out_headers = headers; + return 0; +} + +int smoke_runtime_init(smoke_error *error) { + CURLcode init_rc = curl_global_init(CURL_GLOBAL_DEFAULT); + if (init_rc != CURLE_OK) { + smoke_set_error(error, 0, 0, "TransportError", "curl_global_init failed", curl_easy_strerror(init_rc)); + return -1; + } + return 0; +} + +void smoke_runtime_cleanup(void) { + curl_global_cleanup(); +} + +static int smoke_http_send_request( + const char *base_url, + const smoke_prepared_request *request, + const char *bearer_token, + const struct curl_slist *default_headers, + long timeout_ms, + smoke_http_response *response, + smoke_error *error +) { + const char *http_method; + char *url = NULL; + CURL *curl = NULL; + struct curl_slist *headers = NULL; smoke_buffer buf; + smoke_http_response result; + int rc = -1; + int send_body = 1; + + if (!request || !response) { + smoke_set_error(error, 0, 0, "ClientError", "request and response must be non-NULL", NULL); + return -1; + } + smoke_http_response_init(&result); memset(&buf, 0, sizeof(buf)); + + curl = curl_easy_init(); + if (!curl) { + smoke_set_error(error, 0, 0, "TransportError", "curl_easy_init failed", NULL); + return -1; + } + + url = smoke_join_url(base_url, request->path ? request->path : ""); + if (!url) { + smoke_set_error(error, 0, 0, "TransportError", "failed to build URL", NULL); + curl_easy_cleanup(curl); + return -1; + } + http_method = request->http_method && request->http_method[0] != '\0' ? request->http_method : "POST"; + if (strcmp(http_method, "HEAD") == 0) { + send_body = 0; + } + + if (smoke_build_request_headers(request, bearer_token, default_headers, &headers, error) != 0) { + goto cleanup; + } curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->body ? request->body : ""); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)(request->body ? request->body_len : 0)); + if (send_body) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->body ? request->body : ""); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)(request->body ? request->body_len : 0)); + } else if (strcmp(http_method, "HEAD") == 0) { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms > 0 ? timeout_ms : 10000L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, smoke_write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); - CURLcode rc = curl_easy_perform(curl); - if (rc != CURLE_OK) { - smoke_set_error(error, 0, 0, "TransportError", "HTTP request failed", curl_easy_strerror(rc)); - } else { - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status_code); - result.body = buf.data ? buf.data : smoke_strdup(""); - result.body_len = buf.len; - buf.data = NULL; + { + CURLcode perform_rc = curl_easy_perform(curl); + if (perform_rc != CURLE_OK) { + smoke_set_error(error, 0, 0, "TransportError", "HTTP request failed", curl_easy_strerror(perform_rc)); + goto cleanup; + } } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status_code); + result.body = buf.data ? buf.data : smoke_strdup(""); + result.body_len = buf.len; + buf.data = NULL; + *response = result; + memset(&result, 0, sizeof(result)); + rc = 0; + +cleanup: free(buf.data); curl_slist_free_all(headers); free(url); curl_easy_cleanup(curl); - if (rc != CURLE_OK) { + if (rc != 0) { smoke_http_response_free(&result); - return -1; } - - *response = result; - memset(&result, 0, sizeof(result)); - return 0; + return rc; } static void smoke_parse_rpc_error(const char *body, long http_status, smoke_error *error) { @@ -920,55 +938,77 @@ struct smoke_smoke_client { long timeout_ms; }; +static void smoke_smoke_client_free_config_parts(char **bearer_token, struct curl_slist **default_headers) { + if (bearer_token && *bearer_token) { + free(*bearer_token); + *bearer_token = NULL; + } + if (default_headers && *default_headers) { + curl_slist_free_all(*default_headers); + *default_headers = NULL; + } +} + static void smoke_smoke_client_reset_config(smoke_smoke_client *client) { if (!client) return; - free(client->bearer_token); - client->bearer_token = NULL; - if (client->default_headers) { - curl_slist_free_all(client->default_headers); - client->default_headers = NULL; - } + smoke_smoke_client_free_config_parts(&client->bearer_token, &client->default_headers); client->timeout_ms = 10000L; } -int smoke_smoke_client_configure(smoke_smoke_client *client, const smoke_client_options *options) { - if (!client) return 0; - - smoke_smoke_client_reset_config(client); +static int smoke_smoke_client_build_config( + const smoke_client_options *options, + char **bearer_token, + struct curl_slist **default_headers, + long *timeout_ms +) { + if (!bearer_token || !default_headers || !timeout_ms) return 0; + *bearer_token = NULL; + *default_headers = NULL; + *timeout_ms = 10000L; if (!options) return 1; if (options->timeout_ms > 0) { - client->timeout_ms = options->timeout_ms; + *timeout_ms = options->timeout_ms; } if (options->bearer_token) { - client->bearer_token = smoke_strdup(options->bearer_token); - if (!client->bearer_token) { - smoke_smoke_client_reset_config(client); - return 0; - } + *bearer_token = smoke_strdup(options->bearer_token); + if (!*bearer_token) goto fail; } if (options->headers_count > 0) { - if (!options->headers) { - smoke_smoke_client_reset_config(client); - return 0; - } + if (!options->headers) goto fail; for (size_t i = 0; i < options->headers_count; ++i) { - if (!options->headers[i]) { - smoke_smoke_client_reset_config(client); - return 0; - } - struct curl_slist *next = curl_slist_append(client->default_headers, options->headers[i]); - if (!next) { - smoke_smoke_client_reset_config(client); - return 0; - } - client->default_headers = next; + if (!options->headers[i]) goto fail; + struct curl_slist *next = curl_slist_append(*default_headers, options->headers[i]); + if (!next) goto fail; + *default_headers = next; } } return 1; + +fail: + smoke_smoke_client_free_config_parts(bearer_token, default_headers); + *timeout_ms = 10000L; + return 0; +} + +int smoke_smoke_client_configure(smoke_smoke_client *client, const smoke_client_options *options) { + char *next_bearer_token = NULL; + struct curl_slist *next_default_headers = NULL; + long next_timeout_ms = 10000L; + + if (!client) return 0; + if (!smoke_smoke_client_build_config(options, &next_bearer_token, &next_default_headers, &next_timeout_ms)) { + return 0; + } + + smoke_smoke_client_reset_config(client); + client->bearer_token = next_bearer_token; + client->default_headers = next_default_headers; + client->timeout_ms = next_timeout_ms; + return 1; } smoke_smoke_client *smoke_smoke_client_create(const char *base_url, const smoke_client_options *options) { diff --git a/_examples/smoke/example.gen.h b/_examples/smoke/example.gen.h index ebc7c8d..2ab3864 100644 --- a/_examples/smoke/example.gen.h +++ b/_examples/smoke/example.gen.h @@ -3,7 +3,7 @@ // smoke v1.0.0 6dc82371b24d044c3b3e2bd4cf5d92af46788fff // -- -// Code generated by webrpc-gen@v0.37.1 with .. generator. DO NOT EDIT. +// Code generated by webrpc-gen@v0.37.2 with .. generator. DO NOT EDIT. // // webrpc-gen -schema=smoke/example.ridl -target=.. -emit=header -out=smoke/example.gen.h @@ -227,6 +227,10 @@ static inline const char *smoke_role_to_string(smoke_role value) { static inline int smoke_role_from_string(const char *value, smoke_role *out) { if (!out || !value) return -1; + if (strcmp(value, "UNKNOWN") == 0) { + *out = SMOKE_ROLE_UNKNOWN; + return 0; + } if (strcmp(value, "USER") == 0) { *out = SMOKE_ROLE_USER; return 0; @@ -336,7 +340,10 @@ static inline void smoke_smoke_echo_response_free(smoke_smoke_echo_response *val } memset(value, 0, sizeof(*value)); } - +/* Must be called before using the generated libcurl client runtime. */ +int smoke_runtime_init(smoke_error *error); +/* Call after the last use of the generated libcurl client runtime. */ +void smoke_runtime_cleanup(void); typedef struct smoke_smoke_client smoke_smoke_client; smoke_smoke_client *smoke_smoke_client_create(const char *base_url, const smoke_client_options *options); diff --git a/client.go.tmpl b/client.go.tmpl index f8e2613..4f22a28 100644 --- a/client.go.tmpl +++ b/client.go.tmpl @@ -2,6 +2,11 @@ {{- $prefix := .Prefix -}} {{- $services := .Services -}} +/* Must be called before using the generated libcurl client runtime. */ +int {{ printf "%s_runtime_init" $prefix }}({{ printf "%s_error" $prefix }} *error); +/* Call after the last use of the generated libcurl client runtime. */ +void {{ printf "%s_runtime_cleanup" $prefix }}(void); + {{- range $_, $service := $services }} typedef struct {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }}; diff --git a/generator_test.go b/generator_test.go index db22f31..cdad834 100644 --- a/generator_test.go +++ b/generator_test.go @@ -10,8 +10,8 @@ import ( "testing" ) -const webrpcGenModule = "github.com/webrpc/webrpc/cmd/webrpc-gen@v0.37.1" -const webrpcGenVersion = "v0.37.1" +const webrpcGenModule = "github.com/webrpc/webrpc/cmd/webrpc-gen@v0.37.2" +const webrpcGenVersion = "v0.37.2" func TestGenerateSmoke(t *testing.T) { root := repoRoot(t) @@ -25,6 +25,179 @@ func TestGenerateSmoke(t *testing.T) { syntaxCheckImpl(t, tmp, impl) } +func TestGeneratedTransportDoesNotAutoFollowRedirects(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "smoke.gen.h") + impl := filepath.Join(tmp, "smoke.gen.c") + + generateC(t, root, filepath.Join(root, "testdata", "smoke.ridl"), header, impl, "smoke") + + implText, err := os.ReadFile(impl) + if err != nil { + t.Fatalf("read generated impl: %v", err) + } + implSrc := string(implText) + if strings.Contains(implSrc, "CURLOPT_FOLLOWLOCATION") { + t.Fatalf("generated transport should not auto-follow redirects") + } +} + +func TestGeneratedTransportGuardsResponseBufferOverflow(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "smoke.gen.h") + impl := filepath.Join(tmp, "smoke.gen.c") + + generateC(t, root, filepath.Join(root, "testdata", "smoke.ridl"), header, impl, "smoke") + + implText, err := os.ReadFile(impl) + if err != nil { + t.Fatalf("read generated impl: %v", err) + } + implSrc := string(implText) + if !strings.Contains(implSrc, "nmemb > SIZE_MAX / size") { + t.Fatalf("generated transport should guard size*nmemb overflow") + } + if !strings.Contains(implSrc, "buf->len > SIZE_MAX - n - 1") { + t.Fatalf("generated transport should guard response buffer length overflow") + } +} + +func TestGeneratedIntUintUseFixedWidth32BitTypes(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + schemaPath := filepath.Join(tmp, "fixed-width.ridl") + schemaText := `webrpc = v1 + +name = fixed_width +version = v1.0.0 +basepath = /rpc + +struct Payload + - signed_value: int + - unsigned_value: uint + +service FixedWidth + - Echo(payload: Payload) => (payload: Payload) +` + if err := os.WriteFile(schemaPath, []byte(schemaText), 0o644); err != nil { + t.Fatalf("write fixed-width schema: %v", err) + } + + header := filepath.Join(tmp, "fixed.gen.h") + impl := filepath.Join(tmp, "fixed.gen.c") + generateC(t, root, schemaPath, header, impl, "fixed") + + headerText, err := os.ReadFile(header) + if err != nil { + t.Fatalf("read generated header: %v", err) + } + headerSrc := string(headerText) + if !strings.Contains(headerSrc, "int32_t signed_value;") { + t.Fatalf("generated int should use int32_t") + } + if !strings.Contains(headerSrc, "uint32_t unsigned_value;") { + t.Fatalf("generated uint should use uint32_t") + } +} + +func TestClientConfigureKeepsPreviousConfigOnFailure(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "smoke.gen.h") + impl := filepath.Join(tmp, "smoke.gen.c") + generateC(t, root, filepath.Join(root, "testdata", "smoke.ridl"), header, impl, "smoke") + + testMain := filepath.Join(tmp, "configure_test_main.c") + if err := os.WriteFile(testMain, []byte(configureTestProgram), 0o644); err != nil { + t.Fatalf("write configure test program: %v", err) + } + + cflags := pkgConfigFlags(t, "--cflags") + libs := pkgConfigFlags(t, "--libs") + + bin := filepath.Join(tmp, "configure-test") + args := append([]string{"-std=c99", "-Wall", "-Wextra"}, cflags...) + args = append(args, "configure_test_main.c", "-o", bin) + args = append(args, libs...) + + runCmd(t, tmp, "cc", args...) + runCmd(t, tmp, bin) +} + +func TestEnumUnknownRoundTrips(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "smoke.gen.h") + impl := filepath.Join(tmp, "smoke.gen.c") + generateC(t, root, filepath.Join(root, "testdata", "smoke.ridl"), header, impl, "smoke") + + testMain := filepath.Join(tmp, "enum_unknown_test_main.c") + if err := os.WriteFile(testMain, []byte(enumUnknownTestProgram), 0o644); err != nil { + t.Fatalf("write enum unknown test program: %v", err) + } + + bin := filepath.Join(tmp, "enum-unknown-test") + runCmd(t, tmp, "cc", "-std=c99", "-Wall", "-Wextra", "enum_unknown_test_main.c", "-o", bin) + runCmd(t, tmp, bin) +} + +func TestGenerateFailsWhenEnumUsesReservedUnknownSentinel(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + schemaPath := filepath.Join(tmp, "bad_enum.ridl") + schemaText := `webrpc = v1 + +name = bad_enum +version = v1.0.0 +basepath = /rpc + +enum Status: uint32 + - UNKNOWN + - READY + +service Bad + - Ping() => () +` + if err := os.WriteFile(schemaPath, []byte(schemaText), 0o644); err != nil { + t.Fatalf("write bad enum schema: %v", err) + } + + header := filepath.Join(tmp, "bad.gen.h") + args := []string{ + "run", + "-ldflags=-X github.com/webrpc/webrpc.VERSION=" + webrpcGenVersion, + webrpcGenModule, + "-schema=" + schemaPath, + "-target=" + root, + "-prefix=bad", + "-emit=header", + "-out=" + header, + } + + cmd := exec.Command("go", args...) + cmd.Dir = root + cmd.Env = append(os.Environ(), "GOWORK=off") + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + err := cmd.Run() + if err == nil { + t.Fatal("expected generator to fail for reserved enum UNKNOWN value") + } + if !strings.Contains(stderr.String(), "conflicts with reserved UNKNOWN sentinel") { + t.Fatalf("unexpected generator error: %s", stderr.String()) + } +} + func TestGeneratedCodecBehavior(t *testing.T) { root := repoRoot(t) tmp := t.TempDir() @@ -267,6 +440,88 @@ int main(void) { } ` +const configureTestProgram = `#include +#include +#include + +#include "smoke.gen.c" + +static void fail_msg(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void expect_true(int cond, const char *msg) { + if (!cond) { + fail_msg(msg); + } +} + +int main(void) { + const char *initial_headers[] = {"X-Test: one"}; + smoke_client_options initial; + smoke_error error; + + smoke_client_options_init(&initial); + smoke_error_init(&error); + expect_true(smoke_runtime_init(&error) == 0, "runtime init failed"); + initial.bearer_token = "token1"; + initial.headers = initial_headers; + initial.headers_count = 1; + initial.timeout_ms = 3210L; + + smoke_smoke_client *client = smoke_smoke_client_create("http://example.com", &initial); + expect_true(client != NULL, "client create failed"); + expect_true(client->bearer_token != NULL && strcmp(client->bearer_token, "token1") == 0, "initial bearer mismatch"); + expect_true(client->default_headers != NULL && strcmp(client->default_headers->data, "X-Test: one") == 0, "initial header mismatch"); + expect_true(client->timeout_ms == 3210L, "initial timeout mismatch"); + + smoke_client_options invalid; + smoke_client_options_init(&invalid); + invalid.bearer_token = "token2"; + invalid.headers_count = 1; + invalid.headers = NULL; + invalid.timeout_ms = 9999L; + + expect_true(smoke_smoke_client_configure(client, &invalid) == 0, "configure should fail"); + expect_true(client->bearer_token != NULL && strcmp(client->bearer_token, "token1") == 0, "bearer should be preserved"); + expect_true(client->default_headers != NULL && strcmp(client->default_headers->data, "X-Test: one") == 0, "headers should be preserved"); + expect_true(client->timeout_ms == 3210L, "timeout should be preserved"); + + smoke_smoke_client_destroy(client); + smoke_runtime_cleanup(); + smoke_error_free(&error); + return 0; +} +` + +const enumUnknownTestProgram = `#include +#include +#include + +#include "smoke.gen.h" + +static void fail_msg(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void expect_true(int cond, const char *msg) { + if (!cond) { + fail_msg(msg); + } +} + +int main(void) { + smoke_role role = SMOKE_ROLE_USER; + + expect_true(strcmp(smoke_role_to_string(SMOKE_ROLE_UNKNOWN), "UNKNOWN") == 0, "unknown enum string mismatch"); + expect_true(smoke_role_from_string("UNKNOWN", &role) == 0, "UNKNOWN enum string should decode"); + expect_true(role == SMOKE_ROLE_UNKNOWN, "UNKNOWN enum value mismatch"); + return 0; +} +` + const succinctTestProgram = `#include #include #include diff --git a/implClient.go.tmpl b/implClient.go.tmpl index fd8494e..8608cec 100644 --- a/implClient.go.tmpl +++ b/implClient.go.tmpl @@ -8,55 +8,77 @@ struct {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} { long timeout_ms; }; +static void {{ printf "%s_%s_client_free_config_parts" $prefix (snakeCase $service.Name) }}(char **bearer_token, struct curl_slist **default_headers) { + if (bearer_token && *bearer_token) { + free(*bearer_token); + *bearer_token = NULL; + } + if (default_headers && *default_headers) { + curl_slist_free_all(*default_headers); + *default_headers = NULL; + } +} + static void {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client) { if (!client) return; - free(client->bearer_token); - client->bearer_token = NULL; - if (client->default_headers) { - curl_slist_free_all(client->default_headers); - client->default_headers = NULL; - } + {{ printf "%s_%s_client_free_config_parts" $prefix (snakeCase $service.Name) }}(&client->bearer_token, &client->default_headers); client->timeout_ms = 10000L; } -int {{ printf "%s_%s_client_configure" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, const {{ printf "%s_client_options" $prefix }} *options) { - if (!client) return 0; - - {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); +static int {{ printf "%s_%s_client_build_config" $prefix (snakeCase $service.Name) }}( + const {{ printf "%s_client_options" $prefix }} *options, + char **bearer_token, + struct curl_slist **default_headers, + long *timeout_ms +) { + if (!bearer_token || !default_headers || !timeout_ms) return 0; + *bearer_token = NULL; + *default_headers = NULL; + *timeout_ms = 10000L; if (!options) return 1; if (options->timeout_ms > 0) { - client->timeout_ms = options->timeout_ms; + *timeout_ms = options->timeout_ms; } if (options->bearer_token) { - client->bearer_token = {{ printf "%s_strdup" $prefix }}(options->bearer_token); - if (!client->bearer_token) { - {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); - return 0; - } + *bearer_token = {{ printf "%s_strdup" $prefix }}(options->bearer_token); + if (!*bearer_token) goto fail; } if (options->headers_count > 0) { - if (!options->headers) { - {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); - return 0; - } + if (!options->headers) goto fail; for (size_t i = 0; i < options->headers_count; ++i) { - if (!options->headers[i]) { - {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); - return 0; - } - struct curl_slist *next = curl_slist_append(client->default_headers, options->headers[i]); - if (!next) { - {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); - return 0; - } - client->default_headers = next; + if (!options->headers[i]) goto fail; + struct curl_slist *next = curl_slist_append(*default_headers, options->headers[i]); + if (!next) goto fail; + *default_headers = next; } } return 1; + +fail: + {{ printf "%s_%s_client_free_config_parts" $prefix (snakeCase $service.Name) }}(bearer_token, default_headers); + *timeout_ms = 10000L; + return 0; +} + +int {{ printf "%s_%s_client_configure" $prefix (snakeCase $service.Name) }}({{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *client, const {{ printf "%s_client_options" $prefix }} *options) { + char *next_bearer_token = NULL; + struct curl_slist *next_default_headers = NULL; + long next_timeout_ms = 10000L; + + if (!client) return 0; + if (!{{ printf "%s_%s_client_build_config" $prefix (snakeCase $service.Name) }}(options, &next_bearer_token, &next_default_headers, &next_timeout_ms)) { + return 0; + } + + {{ printf "%s_%s_client_reset_config" $prefix (snakeCase $service.Name) }}(client); + client->bearer_token = next_bearer_token; + client->default_headers = next_default_headers; + client->timeout_ms = next_timeout_ms; + return 1; } {{ printf "%s_%s_client" $prefix (snakeCase $service.Name) }} *{{ printf "%s_%s_client_create" $prefix (snakeCase $service.Name) }}(const char *base_url, const {{ printf "%s_client_options" $prefix }} *options) { diff --git a/implJSONCodec.go.tmpl b/implJSONCodec.go.tmpl index a417ba8..e7b69a7 100644 --- a/implJSONCodec.go.tmpl +++ b/implJSONCodec.go.tmpl @@ -320,7 +320,7 @@ NULL {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected unsigned integer", NULL); goto fail; } - if (parsed > UINT64_C({{ if eq (toString $type) "uint8" }}255{{ else if eq (toString $type) "uint16" }}65535{{ else if eq (toString $type) "uint32" }}4294967295{{ else if eq (toString $type) "byte" }}255{{ else }}4294967295{{ end }})) { + if (parsed > (uint64_t)({{ if eq (toString $type) "uint8" }}UINT8_MAX{{ else if eq (toString $type) "uint16" }}UINT16_MAX{{ else if or (eq (toString $type) "uint32") (eq (toString $type) "uint") }}UINT32_MAX{{ else if eq (toString $type) "byte" }}UINT8_MAX{{ else }}UINT32_MAX{{ end }})) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "unsigned integer out of range", NULL); goto fail; } @@ -342,8 +342,8 @@ NULL {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "expected integer", NULL); goto fail; } - if (parsed < {{ if eq (toString $type) "int8" }}-128LL{{ else if eq (toString $type) "int16" }}-32768LL{{ else if eq (toString $type) "int32" }}-2147483648LL{{ else }}-2147483648LL{{ end }} || - parsed > {{ if eq (toString $type) "int8" }}127LL{{ else if eq (toString $type) "int16" }}32767LL{{ else if eq (toString $type) "int32" }}2147483647LL{{ else }}2147483647LL{{ end }}) { + if (parsed < {{ if eq (toString $type) "int8" }}INT8_MIN{{ else if eq (toString $type) "int16" }}INT16_MIN{{ else if or (eq (toString $type) "int32") (eq (toString $type) "int") }}INT32_MIN{{ else }}INT32_MIN{{ end }} || + parsed > {{ if eq (toString $type) "int8" }}INT8_MAX{{ else if eq (toString $type) "int16" }}INT16_MAX{{ else if or (eq (toString $type) "int32") (eq (toString $type) "int") }}INT32_MAX{{ else }}INT32_MAX{{ end }}) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "DecodeError", "integer out of range", NULL); goto fail; } diff --git a/implTransport.go.tmpl b/implTransport.go.tmpl index a470b09..2b1c367 100644 --- a/implTransport.go.tmpl +++ b/implTransport.go.tmpl @@ -18,9 +18,16 @@ static void {{ printf "%s_set_error" $prefix }}( } static int {{ printf "%s_buffer_grow" $prefix }}({{ printf "%s_buffer" $prefix }} *buf, size_t need) { + if (!buf) return 0; if (buf->cap >= need) return 1; size_t new_cap = buf->cap ? buf->cap : 1024; - while (new_cap < need) new_cap *= 2; + while (new_cap < need) { + if (new_cap > SIZE_MAX / 2) { + new_cap = need; + break; + } + new_cap *= 2; + } char *next = (char *)realloc(buf->data, new_cap); if (!next) return 0; buf->data = next; @@ -29,8 +36,13 @@ static int {{ printf "%s_buffer_grow" $prefix }}({{ printf "%s_buffer" $prefix } } static size_t {{ printf "%s_write_cb" $prefix }}(char *ptr, size_t size, size_t nmemb, void *userdata) { - size_t n = size * nmemb; {{ printf "%s_buffer" $prefix }} *buf = ({{ printf "%s_buffer" $prefix }} *)userdata; + size_t n; + + if (!buf) return 0; + if (size != 0 && nmemb > SIZE_MAX / size) return 0; + n = size * nmemb; + if (buf->len > SIZE_MAX - n - 1) return 0; if (!{{ printf "%s_buffer_grow" $prefix }}(buf, buf->len + n + 1)) return 0; memcpy(buf->data + buf->len, ptr, n); buf->len += n; @@ -132,66 +144,22 @@ static int {{ printf "%s_prepared_request_overrides_header" $prefix }}(const {{ return 0; } -static int {{ printf "%s_curl_runtime_init" $prefix }}({{ printf "%s_error" $prefix }} *error) { - static int initialized = 0; - - if (initialized) { - return 0; - } - - if (curl_global_init(CURL_GLOBAL_DEFAULT) != 0) { - {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "curl_global_init failed", NULL); - return -1; - } - - if (atexit(curl_global_cleanup) != 0) { - curl_global_cleanup(); - {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to register curl_global_cleanup", NULL); - return -1; - } - - initialized = 1; - return 0; -} - -static int {{ printf "%s_http_send_request" $prefix }}( - const char *base_url, +static int {{ printf "%s_build_request_headers" $prefix }}( const {{ printf "%s_prepared_request" $prefix }} *request, const char *bearer_token, const struct curl_slist *default_headers, - long timeout_ms, - {{ printf "%s_http_response" $prefix }} *response, + struct curl_slist **out_headers, {{ printf "%s_error" $prefix }} *error ) { - const char *http_method; - {{ printf "%s_http_response" $prefix }} result; int has_content_type = 0; int has_authorization = 0; - {{ printf "%s_http_response_init" $prefix }}(&result); - - if (!request || !response) { - {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "request and response must be non-NULL", NULL); - return -1; - } - - if ({{ printf "%s_curl_runtime_init" $prefix }}(error) != 0) { - return -1; - } - - CURL *curl = curl_easy_init(); - if (!curl) { - {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "curl_easy_init failed", NULL); - return -1; - } + struct curl_slist *headers = NULL; - char *url = {{ printf "%s_join_url" $prefix }}(base_url, request->path ? request->path : ""); - if (!url) { - {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to build URL", NULL); - curl_easy_cleanup(curl); + if (!request || !out_headers) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "request headers must be built from a valid request", NULL); return -1; } - struct curl_slist *headers = NULL; for (const struct curl_slist *it = default_headers; it; it = it->next) { if ((bearer_token && bearer_token[0] != '\0' && {{ printf "%s_header_name_equals" $prefix }}(it->data, "Authorization")) || {{ printf "%s_prepared_request_overrides_header" $prefix }}(request, it->data)) { @@ -200,8 +168,6 @@ static int {{ printf "%s_http_send_request" $prefix }}( if (!{{ printf "%s_append_header" $prefix }}(&headers, it->data)) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to copy request headers", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if ({{ printf "%s_header_name_equals" $prefix }}(it->data, "Content-Type")) { @@ -218,8 +184,6 @@ static int {{ printf "%s_http_send_request" $prefix }}( if (!content_type_header) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to allocate content-type header", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } snprintf(content_type_header, need, "Content-Type: %s", request->content_type); @@ -227,8 +191,6 @@ static int {{ printf "%s_http_send_request" $prefix }}( {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to append content-type header", NULL); free(content_type_header); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } free(content_type_header); @@ -239,15 +201,11 @@ static int {{ printf "%s_http_send_request" $prefix }}( if (!request->headers || !request->headers[i]) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "request headers are invalid", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if (!{{ printf "%s_append_header" $prefix }}(&headers, request->headers[i])) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to append request header", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if ({{ printf "%s_header_name_equals" $prefix }}(request->headers[i], "Content-Type")) { @@ -261,8 +219,6 @@ static int {{ printf "%s_http_send_request" $prefix }}( if (!has_content_type && !{{ printf "%s_append_header" $prefix }}(&headers, "Content-Type: application/json")) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to allocate request headers", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } if (bearer_token && bearer_token[0] != '\0' && !has_authorization) { @@ -271,8 +227,6 @@ static int {{ printf "%s_http_send_request" $prefix }}( if (!auth_header) { {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to allocate auth header", NULL); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } snprintf(auth_header, need, "Authorization: Bearer %s", bearer_token); @@ -280,49 +234,113 @@ static int {{ printf "%s_http_send_request" $prefix }}( {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to append auth header", NULL); free(auth_header); curl_slist_free_all(headers); - free(url); - curl_easy_cleanup(curl); return -1; } free(auth_header); } + *out_headers = headers; + return 0; +} + +int {{ printf "%s_runtime_init" $prefix }}({{ printf "%s_error" $prefix }} *error) { + CURLcode init_rc = curl_global_init(CURL_GLOBAL_DEFAULT); + if (init_rc != CURLE_OK) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "curl_global_init failed", curl_easy_strerror(init_rc)); + return -1; + } + return 0; +} + +void {{ printf "%s_runtime_cleanup" $prefix }}(void) { + curl_global_cleanup(); +} + +static int {{ printf "%s_http_send_request" $prefix }}( + const char *base_url, + const {{ printf "%s_prepared_request" $prefix }} *request, + const char *bearer_token, + const struct curl_slist *default_headers, + long timeout_ms, + {{ printf "%s_http_response" $prefix }} *response, + {{ printf "%s_error" $prefix }} *error +) { + const char *http_method; + char *url = NULL; + CURL *curl = NULL; + struct curl_slist *headers = NULL; {{ printf "%s_buffer" $prefix }} buf; + {{ printf "%s_http_response" $prefix }} result; + int rc = -1; + int send_body = 1; + + if (!request || !response) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "request and response must be non-NULL", NULL); + return -1; + } + {{ printf "%s_http_response_init" $prefix }}(&result); memset(&buf, 0, sizeof(buf)); + + curl = curl_easy_init(); + if (!curl) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "curl_easy_init failed", NULL); + return -1; + } + + url = {{ printf "%s_join_url" $prefix }}(base_url, request->path ? request->path : ""); + if (!url) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "failed to build URL", NULL); + curl_easy_cleanup(curl); + return -1; + } + http_method = request->http_method && request->http_method[0] != '\0' ? request->http_method : "POST"; + if (strcmp(http_method, "HEAD") == 0) { + send_body = 0; + } + + if ({{ printf "%s_build_request_headers" $prefix }}(request, bearer_token, default_headers, &headers, error) != 0) { + goto cleanup; + } curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->body ? request->body : ""); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)(request->body ? request->body_len : 0)); + if (send_body) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request->body ? request->body : ""); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)(request->body ? request->body_len : 0)); + } else if (strcmp(http_method, "HEAD") == 0) { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms > 0 ? timeout_ms : 10000L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, {{ printf "%s_write_cb" $prefix }}); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); - CURLcode rc = curl_easy_perform(curl); - if (rc != CURLE_OK) { - {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "HTTP request failed", curl_easy_strerror(rc)); - } else { - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status_code); - result.body = buf.data ? buf.data : {{ printf "%s_strdup" $prefix }}(""); - result.body_len = buf.len; - buf.data = NULL; + { + CURLcode perform_rc = curl_easy_perform(curl); + if (perform_rc != CURLE_OK) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "HTTP request failed", curl_easy_strerror(perform_rc)); + goto cleanup; + } } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &result.status_code); + result.body = buf.data ? buf.data : {{ printf "%s_strdup" $prefix }}(""); + result.body_len = buf.len; + buf.data = NULL; + *response = result; + memset(&result, 0, sizeof(result)); + rc = 0; + +cleanup: free(buf.data); curl_slist_free_all(headers); free(url); curl_easy_cleanup(curl); - if (rc != CURLE_OK) { + if (rc != 0) { {{ printf "%s_http_response_free" $prefix }}(&result); - return -1; } - - *response = result; - memset(&result, 0, sizeof(result)); - return 0; + return rc; } static void {{ printf "%s_parse_rpc_error" $prefix }}(const char *body, long http_status, {{ printf "%s_error" $prefix }} *error) { diff --git a/interop_test.go b/interop_test.go index de02681..8af9616 100644 --- a/interop_test.go +++ b/interop_test.go @@ -6,6 +6,7 @@ import ( "io" "net" "net/http" + "net/http/httptest" "os" "os/exec" "path/filepath" @@ -70,6 +71,54 @@ func TestInteropWithWebrpcTest(t *testing.T) { runCmd(t, tmp, bin, serverURL) } +func TestTransportPreservesGetRequestBody(t *testing.T) { + root := repoRoot(t) + tmp := t.TempDir() + + header := filepath.Join(tmp, "smoke.gen.h") + impl := filepath.Join(tmp, "smoke.gen.c") + generateC(t, root, filepath.Join(root, "testdata", "smoke.ridl"), header, impl, "smoke") + + testMain := filepath.Join(tmp, "get_body_test_main.c") + if err := os.WriteFile(testMain, []byte(getBodyCTestProgram), 0o644); err != nil { + t.Fatalf("write GET body test program: %v", err) + } + + cflags := pkgConfigFlags(t, "--cflags") + libs := pkgConfigFlags(t, "--libs") + bin := filepath.Join(tmp, "get-body-test") + args := append([]string{"-std=c99", "-Wall", "-Wextra"}, cflags...) + args = append(args, "get_body_test_main.c", "-o", bin) + args = append(args, libs...) + runCmd(t, tmp, "cc", args...) + + var seenMethod string + var seenBody string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("read request body: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + seenMethod = r.Method + seenBody = string(body) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok":true}`)) + })) + defer server.Close() + + runCmd(t, tmp, bin, server.URL) + + if seenMethod != http.MethodGet { + t.Fatalf("unexpected method: got %q want %q", seenMethod, http.MethodGet) + } + if seenBody != `{"hello":"world"}` { + t.Fatalf("unexpected GET body: got %q", seenBody) + } +} + func ensureWebrpcTestBinary(t *testing.T) string { t.Helper() @@ -278,11 +327,11 @@ int main(int argc, char **argv) { } base_url = argv[1]; + test_error_init(&error); + expect_true(test_runtime_init(&error) == 0, "runtime init failed"); client = test_test_api_client_create(base_url, NULL); expect_true(client != NULL, "failed to create client"); - test_error_init(&error); - { test_test_api_get_empty_request request; test_test_api_get_empty_response response; @@ -495,6 +544,65 @@ int main(int argc, char **argv) { test_error_free(&error); test_test_api_client_destroy(client); + test_runtime_cleanup(); + return 0; +} +` + +const getBodyCTestProgram = `#include +#include +#include + +#include "smoke.gen.c" + +static void fail_msg(const char *msg) { + fprintf(stderr, "%s\n", msg); + exit(1); +} + +static void expect_true(int cond, const char *msg) { + if (!cond) { + fail_msg(msg); + } +} + +int main(int argc, char **argv) { + smoke_smoke_client *client = NULL; + smoke_prepared_request request; + smoke_http_response response; + smoke_error error; + + if (argc != 2) { + fail_msg("expected base URL argument"); + } + + smoke_error_init(&error); + expect_true(smoke_runtime_init(&error) == 0, "runtime init failed"); + client = smoke_smoke_client_create(argv[1], NULL); + expect_true(client != NULL, "client create failed"); + + smoke_prepared_request_init(&request); + smoke_http_response_init(&response); + + request.http_method = smoke_strdup("GET"); + request.path = smoke_strdup("/"); + request.body = smoke_strdup("{\"hello\":\"world\"}"); + request.body_len = strlen(request.body); + request.content_type = smoke_strdup("application/json"); + + expect_true(request.http_method != NULL, "method alloc failed"); + expect_true(request.path != NULL, "path alloc failed"); + expect_true(request.body != NULL, "body alloc failed"); + expect_true(request.content_type != NULL, "content type alloc failed"); + + expect_true(smoke_smoke_client_send_prepared_request(client, &request, &response, &error) == 0, "request failed"); + expect_true(response.status_code == 200, "unexpected status code"); + + smoke_error_free(&error); + smoke_http_response_free(&response); + smoke_prepared_request_free(&request); + smoke_smoke_client_destroy(client); + smoke_runtime_cleanup(); return 0; } ` diff --git a/type.go.tmpl b/type.go.tmpl index 5012317..e5f2f57 100644 --- a/type.go.tmpl +++ b/type.go.tmpl @@ -29,12 +29,12 @@ struct { {{- else if isCoreType $type -}} {{- if eq (toString $type) "byte" -}}uint8_t {{- else if eq (toString $type) "bool" -}}bool - {{- else if eq (toString $type) "uint" -}}unsigned int + {{- else if eq (toString $type) "uint" -}}uint32_t {{- else if eq (toString $type) "uint8" -}}uint8_t {{- else if eq (toString $type) "uint16" -}}uint16_t {{- else if eq (toString $type) "uint32" -}}uint32_t {{- else if eq (toString $type) "uint64" -}}uint64_t - {{- else if eq (toString $type) "int" -}}int + {{- else if eq (toString $type) "int" -}}int32_t {{- else if eq (toString $type) "int8" -}}int8_t {{- else if eq (toString $type) "int16" -}}int16_t {{- else if eq (toString $type) "int32" -}}int32_t diff --git a/types.go.tmpl b/types.go.tmpl index fbae0a2..d53da68 100644 --- a/types.go.tmpl +++ b/types.go.tmpl @@ -14,6 +14,11 @@ static inline void {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Na {{- range $_, $type := $types }} {{- if isEnumType $type }} +{{- range $_, $field := $type.Fields }} +{{- if eq (snakeCase $field.Name) "unknown" }} +{{- fail (printf "C generator error: enum %s value %s conflicts with reserved UNKNOWN sentinel" $type.Name $field.Name) }} +{{- end }} +{{- end }} typedef enum {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} { {{ printf "%s_%s_UNKNOWN" (toUpper $prefix) (toUpper (snakeCase $type.Name)) }} = 0, {{- range $_, $field := $type.Fields }} @@ -33,6 +38,10 @@ static inline const char *{{ template "cTypeName" dict "Prefix" $prefix "Name" $ static inline int {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }}_from_string(const char *value, {{ template "cTypeName" dict "Prefix" $prefix "Name" $type.Name }} *out) { if (!out || !value) return -1; + if (strcmp(value, "UNKNOWN") == 0) { + *out = {{ printf "%s_%s_UNKNOWN" (toUpper $prefix) (toUpper (snakeCase $type.Name)) }}; + return 0; + } {{- range $_, $field := $type.Fields }} if (strcmp(value, "{{$field.Name}}") == 0) { *out = {{ template "cEnumValue" dict "Prefix" $prefix "TypeName" $type.Name "FieldName" $field.Name }};