diff --git a/README.md b/README.md index 1b7a74a..0ec1552 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 a self-contained `libcurl`-based HTTP transport/runtime. + and an optional 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. @@ -21,10 +21,11 @@ The generated client is intended to speak to any webrpc server language Generated `header` output only depends on the C standard library headers included by the generated file. -Generated `impl` output currently depends on: +Generated `impl` output depends on: - `cJSON` -- `libcurl` +- `libcurl`, unless the generated implementation is compiled with the + prefix-based no-curl guard The generated code targets C99. @@ -53,6 +54,33 @@ Dependency names can vary slightly by platform or package manager. The important is that the generated implementation can include `` and link against `libcurl` and `cJSON`. +To build generated implementation output without the built-in libcurl transport, define +`_NO_CURL_TRANSPORT`, where `` is the generated prefix uppercased. For +example, code generated with `-prefix=example` can be compiled with: + +```bash +cc -std=c99 \ + -DEXAMPLE_NO_CURL_TRANSPORT \ + $(pkg-config --cflags libcjson) \ + -c example.gen.c + +cc -std=c99 \ + -DEXAMPLE_NO_CURL_TRANSPORT \ + app.c example.gen.c \ + $(pkg-config --cflags --libs libcjson) \ + -o app +``` + +No-curl mode removes the generated implementation's libcurl include, types, and link +dependency. It does not remove the `cJSON` dependency because generated JSON +encode/decode and response parsing still use `cJSON`. + +The lower-level request/response helpers remain available in no-curl mode: +`example___prepare_request(...)` builds a prepared request and +`example___parse_response(...)` parses an HTTP response supplied by +your own transport. Runtime and client functions still link; send attempts fail with a +`TransportError` indicating that the built-in curl transport is disabled. + Because the generated implementation uses `cJSON`, exact large 64-bit integer handling follows `cJSON`'s numeric behavior. If your API needs exact integer round-tripping beyond normal JSON number precision expectations, prefer `bigint` in the schema instead of @@ -75,7 +103,7 @@ The current generator supports: - prepare request bytes without sending them - send a prepared request with the generated transport - parse a raw HTTP response into generated response types -- generated `libcurl` client configuration for bearer auth, custom headers, and timeouts +- generated client configuration for bearer auth, custom headers, timeouts, and bounded response buffering ## Limitations @@ -87,6 +115,11 @@ The current generator does not support: - a shared external transport abstraction; the generated runtime is currently self-contained - automatic redirect following +Generated client options include `max_response_bytes`, which bounds the response body +buffer used by the built-in curl transport. `*_client_options_init(...)` defaults this +to `1024 * 1024` bytes. Leaving it zero is treated the same as the default, not as an +unlimited response size. + Implementation generation also assumes a companion generated header include via `-header=`. diff --git a/_examples/smoke/example.gen.c b/_examples/smoke/example.gen.c index b8ec801..7e0ce01 100644 --- a/_examples/smoke/example.gen.c +++ b/_examples/smoke/example.gen.c @@ -6,17 +6,22 @@ #include "example.gen.h" +#ifndef SMOKE_NO_CURL_TRANSPORT #include +#endif #include #include #include #include +#ifndef SMOKE_NO_CURL_TRANSPORT typedef struct { char *data; size_t len; size_t cap; + size_t max_response_bytes; } smoke_buffer; +#endif #if defined(__GNUC__) || defined(__clang__) #define SMOKE_JSON_UNUSED __attribute__((unused)) @@ -133,8 +138,16 @@ static void smoke_set_error( error->cause = cause ? smoke_strdup(cause) : NULL; } -static int smoke_buffer_grow(smoke_buffer *buf, size_t need) { +#ifndef SMOKE_NO_CURL_TRANSPORT +static size_t smoke_default_max_response_bytes(void) { + return (size_t)1024 * 1024; +} + +static int smoke_buffer_grow(smoke_buffer *buf, size_t need, size_t max_response_bytes) { + size_t max_cap; if (!buf) return 0; + max_cap = max_response_bytes == SIZE_MAX ? SIZE_MAX : max_response_bytes + 1; + if (need > max_cap) return 0; if (buf->cap >= need) return 1; size_t new_cap = buf->cap ? buf->cap : 1024; while (new_cap < need) { @@ -144,6 +157,9 @@ static int smoke_buffer_grow(smoke_buffer *buf, size_t need) { } new_cap *= 2; } + if (new_cap > max_cap) { + new_cap = max_cap; + } char *next = (char *)realloc(buf->data, new_cap); if (!next) return 0; buf->data = next; @@ -154,12 +170,18 @@ 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) { smoke_buffer *buf = (smoke_buffer *)userdata; size_t n; + size_t need; + size_t max_response_bytes; if (!buf) return 0; if (size != 0 && nmemb > SIZE_MAX / size) return 0; n = size * nmemb; + if (n > SIZE_MAX - 1) return 0; if (buf->len > SIZE_MAX - n - 1) return 0; - if (!smoke_buffer_grow(buf, buf->len + n + 1)) return 0; + need = buf->len + n + 1; + max_response_bytes = buf->max_response_bytes > 0 ? buf->max_response_bytes : smoke_default_max_response_bytes(); + if (need - 1 > max_response_bytes) return 0; + if (!smoke_buffer_grow(buf, need, max_response_bytes)) return 0; memcpy(buf->data + buf->len, ptr, n); buf->len += n; buf->data[buf->len] = '\0'; @@ -378,6 +400,7 @@ static int smoke_http_send_request( const char *bearer_token, const struct curl_slist *default_headers, long timeout_ms, + size_t max_response_bytes, smoke_http_response *response, smoke_error *error ) { @@ -396,6 +419,7 @@ static int smoke_http_send_request( } smoke_http_response_init(&result); memset(&buf, 0, sizeof(buf)); + buf.max_response_bytes = max_response_bytes > 0 ? max_response_bytes : smoke_default_max_response_bytes(); curl = curl_easy_init(); if (!curl) { @@ -458,6 +482,15 @@ static int smoke_http_send_request( } return rc; } +#else +int smoke_runtime_init(smoke_error *error) { + (void)error; + return 0; +} + +void smoke_runtime_cleanup(void) { +} +#endif static void smoke_parse_rpc_error(const char *body, long http_status, smoke_error *error) { cJSON *error_name = NULL; @@ -931,11 +964,13 @@ int smoke_smoke_echo_parse_response( return rc; } +#ifndef SMOKE_NO_CURL_TRANSPORT struct smoke_smoke_client { char *base_url; char *bearer_token; struct curl_slist *default_headers; long timeout_ms; + size_t max_response_bytes; }; static void smoke_smoke_client_free_config_parts(char **bearer_token, struct curl_slist **default_headers) { @@ -953,23 +988,29 @@ static void smoke_smoke_client_reset_config(smoke_smoke_client *client) { if (!client) return; smoke_smoke_client_free_config_parts(&client->bearer_token, &client->default_headers); client->timeout_ms = 10000L; + client->max_response_bytes = (size_t)1024 * 1024; } static int smoke_smoke_client_build_config( const smoke_client_options *options, char **bearer_token, struct curl_slist **default_headers, - long *timeout_ms + long *timeout_ms, + size_t *max_response_bytes ) { - if (!bearer_token || !default_headers || !timeout_ms) return 0; + if (!bearer_token || !default_headers || !timeout_ms || !max_response_bytes) return 0; *bearer_token = NULL; *default_headers = NULL; *timeout_ms = 10000L; + *max_response_bytes = (size_t)1024 * 1024; if (!options) return 1; if (options->timeout_ms > 0) { *timeout_ms = options->timeout_ms; } + if (options->max_response_bytes > 0) { + *max_response_bytes = options->max_response_bytes; + } if (options->bearer_token) { *bearer_token = smoke_strdup(options->bearer_token); @@ -991,6 +1032,7 @@ static int smoke_smoke_client_build_config( fail: smoke_smoke_client_free_config_parts(bearer_token, default_headers); *timeout_ms = 10000L; + *max_response_bytes = (size_t)1024 * 1024; return 0; } @@ -998,9 +1040,10 @@ int smoke_smoke_client_configure(smoke_smoke_client *client, const smoke_client_ char *next_bearer_token = NULL; struct curl_slist *next_default_headers = NULL; long next_timeout_ms = 10000L; + size_t next_max_response_bytes = (size_t)1024 * 1024; if (!client) return 0; - if (!smoke_smoke_client_build_config(options, &next_bearer_token, &next_default_headers, &next_timeout_ms)) { + if (!smoke_smoke_client_build_config(options, &next_bearer_token, &next_default_headers, &next_timeout_ms, &next_max_response_bytes)) { return 0; } @@ -1008,6 +1051,7 @@ int smoke_smoke_client_configure(smoke_smoke_client *client, const smoke_client_ client->bearer_token = next_bearer_token; client->default_headers = next_default_headers; client->timeout_ms = next_timeout_ms; + client->max_response_bytes = next_max_response_bytes; return 1; } @@ -1056,10 +1100,47 @@ int smoke_smoke_client_send_prepared_request( client->bearer_token, client->default_headers, client->timeout_ms, + client->max_response_bytes, response, error ); } +#else +struct smoke_smoke_client { + int placeholder; +}; + +smoke_smoke_client *smoke_smoke_client_create(const char *base_url, const smoke_client_options *options) { + smoke_smoke_client *client; + (void)base_url; + (void)options; + client = (smoke_smoke_client *)calloc(1, sizeof(*client)); + return client; +} + +void smoke_smoke_client_destroy(smoke_smoke_client *client) { + free(client); +} + +int smoke_smoke_client_configure(smoke_smoke_client *client, const smoke_client_options *options) { + (void)options; + return client ? 1 : 0; +} + +int smoke_smoke_client_send_prepared_request( + smoke_smoke_client *client, + const smoke_prepared_request *request, + smoke_http_response *response, + smoke_error *error +) { + if (!client || !request || !response) { + smoke_set_error(error, 0, 0, "ClientError", "client, request, and response must be non-NULL", NULL); + return -1; + } + smoke_set_error(error, 0, 0, "TransportError", "built-in curl transport is disabled", NULL); + return -1; +} +#endif int smoke_smoke_echo( smoke_smoke_client *client, const smoke_smoke_echo_request *request, diff --git a/_examples/smoke/example.gen.h b/_examples/smoke/example.gen.h index 2ab3864..bc47b68 100644 --- a/_examples/smoke/example.gen.h +++ b/_examples/smoke/example.gen.h @@ -54,6 +54,7 @@ typedef struct { const char * const *headers; size_t headers_count; long timeout_ms; + size_t max_response_bytes; } smoke_client_options; static inline char *smoke_strdup(const char *value) { @@ -204,6 +205,7 @@ static inline void smoke_client_options_init(smoke_client_options *options) { if (!options) return; memset(options, 0, sizeof(*options)); options->timeout_ms = 10000L; + options->max_response_bytes = (size_t)1024 * 1024; } @@ -340,9 +342,9 @@ 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. */ +/* Must be called before using the generated client runtime. */ int smoke_runtime_init(smoke_error *error); -/* Call after the last use of the generated libcurl client runtime. */ +/* Call after the last use of the generated client runtime. */ void smoke_runtime_cleanup(void); typedef struct smoke_smoke_client smoke_smoke_client; diff --git a/client.go.tmpl b/client.go.tmpl index b3371bb..ccc13ee 100644 --- a/client.go.tmpl +++ b/client.go.tmpl @@ -2,9 +2,9 @@ {{- $prefix := .Prefix -}} {{- $services := .Services -}} -/* Must be called before using the generated libcurl client runtime. */ +/* Must be called before using the generated client runtime. */ int {{ printf "%s_runtime_init" $prefix }}({{ printf "%s_error" $prefix }} *error); -/* Call after the last use of the generated libcurl client runtime. */ +/* Call after the last use of the generated client runtime. */ void {{ printf "%s_runtime_cleanup" $prefix }}(void); {{- range $_, $service := $services }} diff --git a/generator_test.go b/generator_test.go index 7913c17..9d5e809 100644 --- a/generator_test.go +++ b/generator_test.go @@ -25,6 +25,28 @@ func TestGenerateSmoke(t *testing.T) { syntaxCheckImpl(t, tmp, impl) } +func TestGeneratedClientOptionsIncludeMaxResponseBytes(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") + + headerText, err := os.ReadFile(header) + if err != nil { + t.Fatalf("read generated header: %v", err) + } + headerSrc := string(headerText) + if !strings.Contains(headerSrc, "size_t max_response_bytes;") { + t.Fatalf("generated client options should include max_response_bytes") + } + if !strings.Contains(headerSrc, "options->max_response_bytes = (size_t)1024 * 1024;") { + t.Fatalf("generated client options should default max_response_bytes") + } +} + func TestGeneratedTransportDoesNotAutoFollowRedirects(t *testing.T) { root := repoRoot(t) tmp := t.TempDir() @@ -64,6 +86,68 @@ func TestGeneratedTransportGuardsResponseBufferOverflow(t *testing.T) { if !strings.Contains(implSrc, "buf->len > SIZE_MAX - n - 1") { t.Fatalf("generated transport should guard response buffer length overflow") } + if !strings.Contains(implSrc, "max_response_bytes") { + t.Fatalf("generated transport should include max_response_bytes") + } + if !strings.Contains(implSrc, "need - 1 > max_response_bytes") { + t.Fatalf("generated transport should reject responses beyond max_response_bytes") + } +} + +func TestGeneratedTransportEnforcesResponseBufferLimit(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, "response_limit_test_main.c") + if err := os.WriteFile(testMain, []byte(responseLimitTestProgram), 0o644); err != nil { + t.Fatalf("write response limit test program: %v", err) + } + + cflags := pkgConfigFlags(t, "--cflags") + libs := pkgConfigFlags(t, "--libs") + + bin := filepath.Join(tmp, "response-limit-test") + args := append([]string{"-std=c99", "-Wall", "-Wextra"}, cflags...) + args = append(args, "response_limit_test_main.c", "-o", bin) + args = append(args, libs...) + + runCmd(t, tmp, "cc", args...) + runCmd(t, tmp, bin) +} + +func TestGeneratedNoCurlModeCompilesAndKeepsHelpers(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) + } + assertNoActiveNoCurlTypeRefs(t, string(implText), "SMOKE_NO_CURL_TRANSPORT") + + testMain := filepath.Join(tmp, "no_curl_test_main.c") + if err := os.WriteFile(testMain, []byte(noCurlTestProgram), 0o644); err != nil { + t.Fatalf("write no-curl test program: %v", err) + } + + cflags := pkgConfigCJSONFlags(t, "--cflags") + libs := pkgConfigCJSONFlags(t, "--libs") + + bin := filepath.Join(tmp, "no-curl-test") + args := append([]string{"-std=c99", "-Wall", "-Wextra", "-DSMOKE_NO_CURL_TRANSPORT"}, cflags...) + args = append(args, "no_curl_test_main.c", "-o", bin) + args = append(args, libs...) + + runCmd(t, tmp, "cc", args...) + runCmd(t, tmp, bin) } func TestGeneratedIntUintUseFixedWidth32BitTypes(t *testing.T) { @@ -400,6 +484,86 @@ func pkgConfigFlags(t *testing.T, mode string) []string { return nil } +func pkgConfigCJSONFlags(t *testing.T, mode string) []string { + t.Helper() + + candidates := [][]string{ + {mode, "libcjson"}, + {mode, "cjson"}, + } + + for _, args := range candidates { + cmd := exec.Command("pkg-config", args...) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err == nil { + return strings.Fields(stdout.String()) + } + } + + t.Fatalf("pkg-config failed for cJSON using mode %s", mode) + return nil +} + +func assertNoActiveNoCurlTypeRefs(t *testing.T, src, guard string) { + t.Helper() + + type frame struct { + parent bool + cond bool + } + active := true + var stack []frame + + for lineNo, line := range strings.Split(src, "\n") { + trimmed := strings.TrimSpace(line) + switch { + case strings.HasPrefix(trimmed, "#ifndef "+guard): + stack = append(stack, frame{parent: active, cond: false}) + active = false + continue + case strings.HasPrefix(trimmed, "#ifdef "+guard): + stack = append(stack, frame{parent: active, cond: true}) + active = active + continue + case strings.HasPrefix(trimmed, "#if"): + stack = append(stack, frame{parent: active, cond: true}) + continue + case strings.HasPrefix(trimmed, "#else"): + if len(stack) == 0 { + continue + } + top := &stack[len(stack)-1] + top.cond = !top.cond + active = top.parent && top.cond + continue + case strings.HasPrefix(trimmed, "#endif"): + if len(stack) == 0 { + continue + } + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + active = top.parent + continue + } + + if !active { + continue + } + if strings.Contains(line, " +#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) { + smoke_buffer buf; + char first[] = "abc"; + char second[] = "d"; + + memset(&buf, 0, sizeof(buf)); + buf.max_response_bytes = 3; + + expect_true(smoke_write_cb(first, 1, 3, &buf) == 3, "initial write should fit"); + expect_true(buf.len == 3 && strcmp(buf.data, "abc") == 0, "initial write mismatch"); + expect_true(buf.cap <= buf.max_response_bytes + 1, "buffer capacity should stay within max_response_bytes plus nul"); + expect_true(smoke_write_cb(second, 1, 1, &buf) == 0, "write beyond max_response_bytes should fail"); + expect_true(buf.len == 3 && strcmp(buf.data, "abc") == 0, "failed write should not mutate buffer"); + expect_true(buf.cap <= buf.max_response_bytes + 1, "failed write should not grow beyond max_response_bytes plus nul"); + + free(buf.data); + return 0; +} +` + +const noCurlTestProgram = `#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) { + smoke_client_options options; + smoke_error error; + smoke_prepared_request prepared; + smoke_http_response http_response; + smoke_smoke_echo_response parsed; + + smoke_client_options_init(&options); + smoke_error_init(&error); + smoke_prepared_request_init(&prepared); + smoke_http_response_init(&http_response); + smoke_smoke_echo_response_init(&parsed); + + expect_true(options.max_response_bytes == (size_t)1024 * 1024, "max_response_bytes default mismatch"); + expect_true(smoke_runtime_init(&error) == 0, "no-curl runtime init should succeed"); + + smoke_smoke_client *client = smoke_smoke_client_create("http://example.com", &options); + expect_true(client != NULL, "no-curl client create failed"); + expect_true(smoke_smoke_client_configure(client, &options) == 1, "no-curl configure should succeed"); + + expect_true(smoke_smoke_echo_prepare_request(NULL, &prepared, &error) != 0, "prepare helper should be available"); + smoke_error_free(&error); + expect_true(smoke_smoke_echo_parse_response(NULL, &parsed, &error) != 0, "parse helper should be available"); + smoke_error_free(&error); + + expect_true(smoke_smoke_client_send_prepared_request(client, &prepared, &http_response, &error) != 0, "no-curl send should fail"); + expect_true(error.name != NULL && strcmp(error.name, "TransportError") == 0, "no-curl send error name mismatch"); + expect_true(error.message != NULL && strstr(error.message, "curl transport is disabled") != NULL, "no-curl send error message mismatch"); + + smoke_smoke_client_destroy(client); + smoke_runtime_cleanup(); + smoke_prepared_request_free(&prepared); + smoke_http_response_free(&http_response); + smoke_smoke_echo_response_free(&parsed); + smoke_error_free(&error); + return 0; +} +` + const configureTestProgram = `#include #include #include @@ -561,12 +818,14 @@ int main(void) { initial.headers = initial_headers; initial.headers_count = 1; initial.timeout_ms = 3210L; + initial.max_response_bytes = 12345; 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"); + expect_true(client->max_response_bytes == 12345, "initial max response mismatch"); smoke_client_options invalid; smoke_client_options_init(&invalid); @@ -574,11 +833,13 @@ int main(void) { invalid.headers_count = 1; invalid.headers = NULL; invalid.timeout_ms = 9999L; + invalid.max_response_bytes = 999; 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"); + expect_true(client->max_response_bytes == 12345, "max response should be preserved"); smoke_smoke_client_destroy(client); smoke_runtime_cleanup(); diff --git a/implClient.go.tmpl b/implClient.go.tmpl index 063866e..61bf23f 100644 --- a/implClient.go.tmpl +++ b/implClient.go.tmpl @@ -2,11 +2,13 @@ {{- $prefix := .Prefix -}} {{- range $_, $service := .Services }} {{- $serviceName := (snakeCase (regexReplaceAll "([A-Z]+)([A-Z][a-z])" $service.Name "${1}_${2}")) }} +#ifndef {{ toUpper $prefix }}_NO_CURL_TRANSPORT struct {{ printf "%s_%s_client" $prefix $serviceName }} { char *base_url; char *bearer_token; struct curl_slist *default_headers; long timeout_ms; + size_t max_response_bytes; }; static void {{ printf "%s_%s_client_free_config_parts" $prefix $serviceName }}(char **bearer_token, struct curl_slist **default_headers) { @@ -24,23 +26,29 @@ static void {{ printf "%s_%s_client_reset_config" $prefix $serviceName }}({{ pri if (!client) return; {{ printf "%s_%s_client_free_config_parts" $prefix $serviceName }}(&client->bearer_token, &client->default_headers); client->timeout_ms = 10000L; + client->max_response_bytes = (size_t)1024 * 1024; } static int {{ printf "%s_%s_client_build_config" $prefix $serviceName }}( const {{ printf "%s_client_options" $prefix }} *options, char **bearer_token, struct curl_slist **default_headers, - long *timeout_ms + long *timeout_ms, + size_t *max_response_bytes ) { - if (!bearer_token || !default_headers || !timeout_ms) return 0; + if (!bearer_token || !default_headers || !timeout_ms || !max_response_bytes) return 0; *bearer_token = NULL; *default_headers = NULL; *timeout_ms = 10000L; + *max_response_bytes = (size_t)1024 * 1024; if (!options) return 1; if (options->timeout_ms > 0) { *timeout_ms = options->timeout_ms; } + if (options->max_response_bytes > 0) { + *max_response_bytes = options->max_response_bytes; + } if (options->bearer_token) { *bearer_token = {{ printf "%s_strdup" $prefix }}(options->bearer_token); @@ -62,6 +70,7 @@ static int {{ printf "%s_%s_client_build_config" $prefix $serviceName }}( fail: {{ printf "%s_%s_client_free_config_parts" $prefix $serviceName }}(bearer_token, default_headers); *timeout_ms = 10000L; + *max_response_bytes = (size_t)1024 * 1024; return 0; } @@ -69,9 +78,10 @@ int {{ printf "%s_%s_client_configure" $prefix $serviceName }}({{ printf "%s_%s_ char *next_bearer_token = NULL; struct curl_slist *next_default_headers = NULL; long next_timeout_ms = 10000L; + size_t next_max_response_bytes = (size_t)1024 * 1024; if (!client) return 0; - if (!{{ printf "%s_%s_client_build_config" $prefix $serviceName }}(options, &next_bearer_token, &next_default_headers, &next_timeout_ms)) { + if (!{{ printf "%s_%s_client_build_config" $prefix $serviceName }}(options, &next_bearer_token, &next_default_headers, &next_timeout_ms, &next_max_response_bytes)) { return 0; } @@ -79,6 +89,7 @@ int {{ printf "%s_%s_client_configure" $prefix $serviceName }}({{ printf "%s_%s_ client->bearer_token = next_bearer_token; client->default_headers = next_default_headers; client->timeout_ms = next_timeout_ms; + client->max_response_bytes = next_max_response_bytes; return 1; } @@ -127,10 +138,47 @@ int {{ printf "%s_%s_client_send_prepared_request" $prefix $serviceName }}( client->bearer_token, client->default_headers, client->timeout_ms, + client->max_response_bytes, response, error ); } +#else +struct {{ printf "%s_%s_client" $prefix $serviceName }} { + int placeholder; +}; + +{{ printf "%s_%s_client" $prefix $serviceName }} *{{ printf "%s_%s_client_create" $prefix $serviceName }}(const char *base_url, const {{ printf "%s_client_options" $prefix }} *options) { + {{ printf "%s_%s_client" $prefix $serviceName }} *client; + (void)base_url; + (void)options; + client = ({{ printf "%s_%s_client" $prefix $serviceName }} *)calloc(1, sizeof(*client)); + return client; +} + +void {{ printf "%s_%s_client_destroy" $prefix $serviceName }}({{ printf "%s_%s_client" $prefix $serviceName }} *client) { + free(client); +} + +int {{ printf "%s_%s_client_configure" $prefix $serviceName }}({{ printf "%s_%s_client" $prefix $serviceName }} *client, const {{ printf "%s_client_options" $prefix }} *options) { + (void)options; + return client ? 1 : 0; +} + +int {{ printf "%s_%s_client_send_prepared_request" $prefix $serviceName }}( + {{ printf "%s_%s_client" $prefix $serviceName }} *client, + const {{ printf "%s_prepared_request" $prefix }} *request, + {{ printf "%s_http_response" $prefix }} *response, + {{ printf "%s_error" $prefix }} *error +) { + if (!client || !request || !response) { + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "ClientError", "client, request, and response must be non-NULL", NULL); + return -1; + } + {{ printf "%s_set_error" $prefix }}(error, 0, 0, "TransportError", "built-in curl transport is disabled", NULL); + return -1; +} +#endif {{- range $_, $method := $service.Methods }} {{- $methodName := (snakeCase (regexReplaceAll "([A-Z]+)([A-Z][a-z])" $method.Name "${1}_${2}")) }} diff --git a/implPreamble.go.tmpl b/implPreamble.go.tmpl index 7ee914f..5aca171 100644 --- a/implPreamble.go.tmpl +++ b/implPreamble.go.tmpl @@ -9,17 +9,22 @@ #include "{{$header}}" +#ifndef {{ toUpper $prefix }}_NO_CURL_TRANSPORT #include +#endif #include #include #include #include +#ifndef {{ toUpper $prefix }}_NO_CURL_TRANSPORT typedef struct { char *data; size_t len; size_t cap; + size_t max_response_bytes; } {{ printf "%s_buffer" $prefix }}; +#endif #if defined(__GNUC__) || defined(__clang__) #define {{ toUpper $prefix }}_JSON_UNUSED __attribute__((unused)) diff --git a/implTransport.go.tmpl b/implTransport.go.tmpl index 2b1c367..c63c5b5 100644 --- a/implTransport.go.tmpl +++ b/implTransport.go.tmpl @@ -17,8 +17,16 @@ static void {{ printf "%s_set_error" $prefix }}( error->cause = cause ? {{ printf "%s_strdup" $prefix }}(cause) : NULL; } -static int {{ printf "%s_buffer_grow" $prefix }}({{ printf "%s_buffer" $prefix }} *buf, size_t need) { +#ifndef {{ toUpper $prefix }}_NO_CURL_TRANSPORT +static size_t {{ printf "%s_default_max_response_bytes" $prefix }}(void) { + return (size_t)1024 * 1024; +} + +static int {{ printf "%s_buffer_grow" $prefix }}({{ printf "%s_buffer" $prefix }} *buf, size_t need, size_t max_response_bytes) { + size_t max_cap; if (!buf) return 0; + max_cap = max_response_bytes == SIZE_MAX ? SIZE_MAX : max_response_bytes + 1; + if (need > max_cap) return 0; if (buf->cap >= need) return 1; size_t new_cap = buf->cap ? buf->cap : 1024; while (new_cap < need) { @@ -28,6 +36,9 @@ static int {{ printf "%s_buffer_grow" $prefix }}({{ printf "%s_buffer" $prefix } } new_cap *= 2; } + if (new_cap > max_cap) { + new_cap = max_cap; + } char *next = (char *)realloc(buf->data, new_cap); if (!next) return 0; buf->data = next; @@ -38,12 +49,18 @@ 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) { {{ printf "%s_buffer" $prefix }} *buf = ({{ printf "%s_buffer" $prefix }} *)userdata; size_t n; + size_t need; + size_t max_response_bytes; if (!buf) return 0; if (size != 0 && nmemb > SIZE_MAX / size) return 0; n = size * nmemb; + if (n > SIZE_MAX - 1) return 0; if (buf->len > SIZE_MAX - n - 1) return 0; - if (!{{ printf "%s_buffer_grow" $prefix }}(buf, buf->len + n + 1)) return 0; + need = buf->len + n + 1; + max_response_bytes = buf->max_response_bytes > 0 ? buf->max_response_bytes : {{ printf "%s_default_max_response_bytes" $prefix }}(); + if (need - 1 > max_response_bytes) return 0; + if (!{{ printf "%s_buffer_grow" $prefix }}(buf, need, max_response_bytes)) return 0; memcpy(buf->data + buf->len, ptr, n); buf->len += n; buf->data[buf->len] = '\0'; @@ -262,6 +279,7 @@ static int {{ printf "%s_http_send_request" $prefix }}( const char *bearer_token, const struct curl_slist *default_headers, long timeout_ms, + size_t max_response_bytes, {{ printf "%s_http_response" $prefix }} *response, {{ printf "%s_error" $prefix }} *error ) { @@ -280,6 +298,7 @@ static int {{ printf "%s_http_send_request" $prefix }}( } {{ printf "%s_http_response_init" $prefix }}(&result); memset(&buf, 0, sizeof(buf)); + buf.max_response_bytes = max_response_bytes > 0 ? max_response_bytes : {{ printf "%s_default_max_response_bytes" $prefix }}(); curl = curl_easy_init(); if (!curl) { @@ -342,6 +361,15 @@ cleanup: } return rc; } +#else +int {{ printf "%s_runtime_init" $prefix }}({{ printf "%s_error" $prefix }} *error) { + (void)error; + return 0; +} + +void {{ printf "%s_runtime_cleanup" $prefix }}(void) { +} +#endif static void {{ printf "%s_parse_rpc_error" $prefix }}(const char *body, long http_status, {{ printf "%s_error" $prefix }} *error) { cJSON *error_name = NULL; diff --git a/main.go.tmpl b/main.go.tmpl index 1fd9389..e5faaf3 100644 --- a/main.go.tmpl +++ b/main.go.tmpl @@ -96,6 +96,7 @@ typedef struct { const char * const *headers; size_t headers_count; long timeout_ms; + size_t max_response_bytes; } {{ printf "%s_client_options" $prefix }}; static inline char *{{ printf "%s_strdup" $prefix }}(const char *value) { @@ -246,6 +247,7 @@ static inline void {{ printf "%s_client_options_init" $prefix }}({{ printf "%s_c if (!options) return; memset(options, 0, sizeof(*options)); options->timeout_ms = 10000L; + options->max_response_bytes = (size_t)1024 * 1024; } {{ template "types" dict "Prefix" $prefix "Types" .Types "Services" .Services }}