cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
bind.c
Go to the documentation of this file.
1
8#include <string.h>
9#include <stdlib.h>
10#include <stdio.h>
11#include <errno.h>
12#include <math.h>
13
14/* -------------------------------------------------------------------------
15 * Internal helpers
16 * ---------------------------------------------------------------------- */
17
21static void bind_add_error(cwist_bind_result_t *r, const char *key, const char *msg) {
22 if (!r) return;
23 if (r->error_count >= CWIST_BIND_MAX_ERRORS) return;
25 snprintf(e->key, sizeof(e->key), "%s", key ? key : "(unknown)");
26 snprintf(e->message, sizeof(e->message), "%s", msg ? msg : "validation failed");
27 r->ok = false;
28}
29
34static bool is_email_like(const char *s, size_t len) {
35 if (!s || len < 5) return false;
36 const char *at = strchr(s, '@');
37 if (!at) return false;
38 if (at == s) return false;
39 const char *dot = strrchr(at, '.');
40 if (!dot || dot <= at + 1) return false;
41 if (dot + 1 == s + len) return false; /* trailing dot */
42 return true;
43}
44
49static bool check_rule(const cwist_bind_rule_t *rule, const char *value, size_t len,
50 cwist_bind_result_t *r, const char *key) {
51 char buf[CWIST_BIND_MSG_MAX];
52
53 switch (rule->type) {
55 if (!value || len == 0) {
56 snprintf(buf, sizeof(buf), "%s",
57 rule->error_message ? rule->error_message : "field is required");
58 bind_add_error(r, key, buf);
59 return false;
60 }
61 break;
62
64 if (!value || len < rule->u.min_len) {
65 snprintf(buf, sizeof(buf), "%s: minimum length is %zu",
66 rule->error_message ? rule->error_message : "too short", rule->u.min_len);
67 bind_add_error(r, key, buf);
68 return false;
69 }
70 break;
71
73 if (value && len > rule->u.max_len) {
74 snprintf(buf, sizeof(buf), "%s: maximum length is %zu",
75 rule->error_message ? rule->error_message : "too long", rule->u.max_len);
76 bind_add_error(r, key, buf);
77 return false;
78 }
79 break;
80
82 long double v = 0.0L;
83 if (value) v = strtold(value, NULL);
84 if (!value || v < rule->u.min_val) {
85 snprintf(buf, sizeof(buf), "%s: minimum value is %Lg",
86 rule->error_message ? rule->error_message : "value too small",
87 rule->u.min_val);
88 bind_add_error(r, key, buf);
89 return false;
90 }
91 break;
92 }
93
95 long double v = 0.0L;
96 if (value) v = strtold(value, NULL);
97 if (!value || v > rule->u.max_val) {
98 snprintf(buf, sizeof(buf), "%s: maximum value is %Lg",
99 rule->error_message ? rule->error_message : "value too large",
100 rule->u.max_val);
101 bind_add_error(r, key, buf);
102 return false;
103 }
104 break;
105 }
106
108 if (!value) {
109 bind_add_error(r, key, "missing value for regex check");
110 return false;
111 }
112 regex_t re;
113 int rc = regcomp(&re, rule->u.pattern, REG_EXTENDED | REG_NOSUB);
114 if (rc != 0) {
115 bind_add_error(r, key, "invalid regex pattern");
116 return false;
117 }
118 rc = regexec(&re, value, 0, NULL, 0);
119 regfree(&re);
120 if (rc != 0) {
121 snprintf(buf, sizeof(buf), "%s",
122 rule->error_message ? rule->error_message
123 : "value does not match pattern");
124 bind_add_error(r, key, buf);
125 return false;
126 }
127 break;
128 }
129
131 if (!value || !is_email_like(value, len)) {
132 snprintf(buf, sizeof(buf), "%s",
133 rule->error_message ? rule->error_message : "invalid email format");
134 bind_add_error(r, key, buf);
135 return false;
136 }
137 break;
138
140 if (!value || !rule->u.custom.fn(value, len, rule->u.custom.ctx)) {
141 snprintf(buf, sizeof(buf), "%s",
142 rule->error_message ? rule->error_message : "custom validation failed");
143 bind_add_error(r, key, buf);
144 return false;
145 }
146 break;
147
148 default: break;
149 }
150 return true;
151}
152
156static bool run_rules(const cwist_bind_field_t *f, const char *value, size_t len,
158 if (!f->rules) return true;
159 bool all_ok = true;
160 for (size_t i = 0; i < CWIST_BIND_MAX_ERRORS; ++i) {
161 if (f->rules[i].type == (cwist_bind_rule_type_t)-1) break;
162 if (!check_rule(&f->rules[i], value, len, r, f->json_key)) all_ok = false;
163 }
164 return all_ok;
165}
166
170static bool write_value(const cwist_bind_field_t *f, const char *value, void *out,
172 void *dest = (char *)out + f->target_offset;
173
174 switch (f->target_type) {
175 case CWIST_BIND_BOOL: {
176 bool v = false;
177 if (value) {
178 if (strcmp(value, "true") == 0 || strcmp(value, "1") == 0)
179 v = true;
180 else if (strcmp(value, "false") == 0 || strcmp(value, "0") == 0)
181 v = false;
182 else {
183 bind_add_error(r, f->json_key, "expected boolean");
184 return false;
185 }
186 }
187 memcpy(dest, &v, sizeof(v));
188 break;
189 }
190 case CWIST_BIND_INT: {
191 long long v = 0;
192 if (value) {
193 char *end = NULL;
194 errno = 0;
195 v = strtoll(value, &end, 10);
196 if (errno == ERANGE || end == value || *end != '\0') {
197 bind_add_error(r, f->json_key, "expected integer");
198 return false;
199 }
200 }
201 memcpy(dest, &v, f->target_size <= sizeof(v) ? f->target_size : sizeof(v));
202 break;
203 }
204 case CWIST_BIND_UINT: {
205 unsigned long long v = 0;
206 if (value) {
207 char *end = NULL;
208 errno = 0;
209 v = strtoull(value, &end, 10);
210 if (errno == ERANGE || end == value || *end != '\0') {
211 bind_add_error(r, f->json_key, "expected unsigned integer");
212 return false;
213 }
214 }
215 memcpy(dest, &v, f->target_size <= sizeof(v) ? f->target_size : sizeof(v));
216 break;
217 }
218 case CWIST_BIND_FLOAT: {
219 float v = 0.0f;
220 if (value) {
221 char *end = NULL;
222 v = strtof(value, &end);
223 if (end == value || *end != '\0') {
224 bind_add_error(r, f->json_key, "expected float");
225 return false;
226 }
227 }
228 memcpy(dest, &v, sizeof(v));
229 break;
230 }
231 case CWIST_BIND_DOUBLE: {
232 double v = 0.0;
233 if (value) {
234 char *end = NULL;
235 v = strtod(value, &end);
236 if (end == value || *end != '\0') {
237 bind_add_error(r, f->json_key, "expected double");
238 return false;
239 }
240 }
241 memcpy(dest, &v, sizeof(v));
242 break;
243 }
244 case CWIST_BIND_STRING: {
245 if (f->target_size == 0) {
246 bind_add_error(r, f->json_key, "bad target size");
247 return false;
248 }
249 if (value) {
250 size_t copy = strlen(value);
251 if (copy >= f->target_size) copy = f->target_size - 1;
252 memcpy(dest, value, copy);
253 ((char *)dest)[copy] = '\0';
254 } else {
255 ((char *)dest)[0] = '\0';
256 }
257 break;
258 }
259 case CWIST_BIND_SSTRING: {
261 if (!ss) {
262 bind_add_error(r, f->json_key, "null sstring target");
263 return false;
264 }
265 if (value)
266 cwist_sstring_assign(ss, (char *)value);
267 else
269 break;
270 }
272 cJSON **pp = (cJSON **)dest;
273 if (!pp) {
274 bind_add_error(r, f->json_key, "null cJSON target");
275 return false;
276 }
277 if (*pp) {
279 *pp = NULL;
280 }
281 if (value) {
282 *pp = cJSON_Parse(value);
283 if (!*pp) {
284 bind_add_error(r, f->json_key, "invalid nested JSON");
285 return false;
286 }
287 }
288 break;
289 }
290 default: bind_add_error(r, f->json_key, "unsupported bind target type"); return false;
291 }
292 return true;
293}
294
298static bool cwist_bind_generic(const cwist_bind_schema_t *schema, const char *payload, bool is_json,
299 void *out, cwist_bind_result_t *result) {
300 if (!schema || !out || !result) return false;
301 memset(result, 0, sizeof(*result));
302 result->ok = true;
303
304 if (!payload) {
305 bind_add_error(result, "(root)", "missing request body");
306 return false;
307 }
308
309 cJSON *root = NULL;
310 if (is_json) {
311 root = cJSON_Parse(payload);
312 if (!root) {
313 bind_add_error(result, "(root)", "JSON parse error");
314 return false;
315 }
316 }
317
318 for (size_t i = 0; i < schema->field_count; ++i) {
319 const cwist_bind_field_t *f = &schema->fields[i];
320 const char *raw_value = NULL;
321
322 if (is_json && root) {
323 const cJSON *item = cJSON_GetObjectItemCaseSensitive(root, f->json_key);
324 if (item) {
325 if (cJSON_IsString(item)) {
326 raw_value = item->valuestring;
327 } else if (cJSON_IsNumber(item)) {
328 /* Keep number as string for uniform validation then convert */
329 static __thread char numbuf[64];
330 double intpart;
331 if (modf(item->valuedouble, &intpart) == 0.0) {
332 snprintf(numbuf, sizeof(numbuf), "%lld", (long long)item->valuedouble);
333 } else {
334 snprintf(numbuf, sizeof(numbuf), "%g", item->valuedouble);
335 }
337 } else if (cJSON_IsBool(item)) {
338 raw_value = cJSON_IsTrue(item) ? "true" : "false";
339 } else if (cJSON_IsNull(item)) {
340 raw_value = NULL;
341 } else if (cJSON_IsObject(item) || cJSON_IsArray(item)) {
342 /* For JSON_OBJECT targets, serialise the subtree */
343 if (f->target_type == CWIST_BIND_JSON_OBJECT) {
345 raw_value = printed; /* transient; written below */
346 write_value(f, raw_value, out, result);
347 free(printed);
348 continue;
349 } else {
351 /* fall through to string validation then cleanup */
352 }
353 }
354 }
355 } else {
356 /* Form parsing: simple key=value scan (naïve, no unescaping) */
357 const char *cursor = payload;
358 size_t key_len = strlen(f->json_key);
359 while (cursor && *cursor) {
360 if (strncmp(cursor, f->json_key, key_len) == 0 && cursor[key_len] == '=') {
361 const char *start = cursor + key_len + 1;
362 const char *end = strchr(start, '&');
363 size_t vlen = end ? (size_t)(end - start) : strlen(start);
364 /* Zero-copy reference into payload; safe because req->body is stable */
366 /* Temporarily null-terminate for validation (destructive to body). */
367 /* We make a bounded copy instead to be safe. */
368 if (vlen > 0) {
369 static __thread char formbuf[4096];
370 if (vlen >= sizeof(formbuf)) vlen = sizeof(formbuf) - 1;
372 formbuf[vlen] = '\0';
374 } else {
375 raw_value = "";
376 }
377 break;
378 }
379 const char *next = strchr(cursor, '&');
380 cursor = next ? next + 1 : NULL;
381 }
382 }
383
384 size_t val_len = raw_value ? strlen(raw_value) : 0;
385
386 if (!run_rules(f, raw_value, val_len, result)) {
387 /* continue to collect more errors */
388 }
389
390 if (result->error_count == 0 || result->ok) {
391 if (!write_value(f, raw_value, out, result)) {
392 result->ok = false;
393 }
394 }
395
396 /* Clean up temporary printed JSON if we allocated one above */
397 if (!is_json) { /* already handled */
398 }
399 }
400
401 if (root) cJSON_Delete(root);
402 result->ok = (result->error_count == 0);
403 return result->ok;
404}
405
406/* -------------------------------------------------------------------------
407 * Public implementation
408 * ---------------------------------------------------------------------- */
409
427 cwist_bind_result_t *result) {
428 const char *payload = (req && req->body && req->body->data) ? req->body->data : NULL;
429 return cwist_bind_generic(schema, payload, true, out, result);
430}
431
447 cwist_bind_result_t *result) {
448 const char *payload = (req && req->body && req->body->data) ? req->body->data : NULL;
449 return cwist_bind_generic(schema, payload, false, out, result);
450}
451
464 if (!res || !result) return;
466 cwist_sstring_assign(res->status_text, "Bad Request");
467
470 cwist_json_add_bool(jb, "success", false);
471 cwist_json_begin_array(jb, "errors");
472 for (size_t i = 0; i < result->error_count; ++i) {
474 cwist_json_add_string(jb, "field", result->errors[i].key);
475 cwist_json_add_string(jb, "message", result->errors[i].message);
477 }
480
481 const char *json = cwist_json_get_raw(jb);
482 cwist_sstring_assign(res->body, (char *)json);
483 cwist_http_header_add(&res->headers, "Content-Type", "application/json");
485}
486
503 const cwist_bind_schema_t *schema, void *out) {
504 cwist_bind_result_t result;
505 if (!cwist_app_req_bind_json(req, schema, out, &result)) {
507 return false;
508 }
509 return true;
510}
Memory allocation wrappers backed by libttak.
bool cwist_app_req_bind_json_or_400(cwist_http_request *req, cwist_http_response *res, const cwist_bind_schema_t *schema, void *out)
Bind a JSON request body, writing a 400 error response on failure.
Definition bind.c:502
bool cwist_app_req_bind_form(cwist_http_request *req, const cwist_bind_schema_t *schema, void *out, cwist_bind_result_t *result)
Validate and bind a form-encoded (key=value) request body into a struct per the schema.
Definition bind.c:446
bool cwist_app_req_bind_json(cwist_http_request *req, const cwist_bind_schema_t *schema, void *out, cwist_bind_result_t *result)
Validate and bind a JSON request body into a struct per the schema.
Definition bind.c:426
void cwist_bind_write_error_response(cwist_http_response *res, const cwist_bind_result_t *result)
Write a 400 Bad Request JSON error response from a bind result.
Definition bind.c:463
Declarative Schema Validation and Type-Safe Binding for CWIST.
#define CWIST_BIND_MAX_ERRORS
Definition bind.h:94
#define CWIST_BIND_MSG_MAX
Definition bind.h:95
@ CWIST_BIND_DOUBLE
Definition bind.h:67
@ CWIST_BIND_JSON_OBJECT
cJSON * (ownership transferred to caller).
Definition bind.h:70
@ CWIST_BIND_INT
Definition bind.h:64
@ CWIST_BIND_UINT
Definition bind.h:65
@ CWIST_BIND_FLOAT
Definition bind.h:66
@ CWIST_BIND_STRING
Null-terminated C string (copied).
Definition bind.h:68
@ CWIST_BIND_BOOL
Definition bind.h:63
@ CWIST_BIND_SSTRING
cwist_sstring target.
Definition bind.h:69
enum cwist_bind_rule_type cwist_bind_rule_type_t
Supported validation constraint types.
@ CWIST_BIND_RULE_CUSTOM
User-supplied predicate.
Definition bind.h:41
@ CWIST_BIND_RULE_MAX_LEN
Maximum string length.
Definition bind.h:36
@ CWIST_BIND_RULE_MAX_VAL
Maximum numeric value.
Definition bind.h:38
@ CWIST_BIND_RULE_EMAIL
Must conform to RFC 5322 simplified email shape.
Definition bind.h:40
@ CWIST_BIND_RULE_REQUIRED
Field must be present and non-empty.
Definition bind.h:34
@ CWIST_BIND_RULE_REGEX
Must match POSIX extended regular expression.
Definition bind.h:39
@ CWIST_BIND_RULE_MIN_VAL
Minimum numeric value.
Definition bind.h:37
@ CWIST_BIND_RULE_MIN_LEN
Minimum string length.
Definition bind.h:35
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
const char * cwist_json_get_raw(cwist_json_builder *b)
Returns the raw string buffer. Invalidated on destroy.
void cwist_json_add_string(cwist_json_builder *b, const char *key, const char *value)
Add a string value.
void cwist_json_begin_object(cwist_json_builder *b)
Begin a JSON object.
void cwist_json_end_object(cwist_json_builder *b)
End a JSON object.
void cwist_json_builder_destroy(cwist_json_builder *b)
Destroy a JSON builder.
void cwist_json_add_bool(cwist_json_builder *b, const char *key, bool value)
Add a boolean value.
cwist_json_builder * cwist_json_builder_create(void)
Create a new JSON builder.
void cwist_json_begin_array(cwist_json_builder *b, const char *key)
Begin a JSON array.
void cwist_json_end_array(cwist_json_builder *b)
End a JSON array.
@ CWIST_HTTP_BAD_REQUEST
Definition http.h:31
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Add a header to the list.
Definition http.c:1861
size_t cap
Definition otel.c:56
A single bind/validation failure.
Definition bind.h:99
char key[CWIST_BIND_KEY_MAX]
Definition bind.h:100
char message[CWIST_BIND_MSG_MAX]
Definition bind.h:101
Per-field binding descriptor.
Definition bind.h:74
Result of a bind operation.
Definition bind.h:105
cwist_bind_error_t errors[CWIST_BIND_MAX_ERRORS]
Definition bind.h:107
size_t error_count
Definition bind.h:108
Single validation rule attached to a field.
Definition bind.h:45
Complete schema descriptor for a struct.
Definition bind.h:84
const cwist_bind_field_t * fields
Definition bind.h:85
size_t field_count
Definition bind.h:86
cwist_sstring * body
Definition http.h:73
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_sstring * status_text
e.g., "OK"
Definition http.h:85
cwist_sstring * body
Definition http.h:87
cwist_http_status_t status_code
Definition http.h:84
cwist_http_header_node * headers
Definition http.h:86
Simple JSON string builder utility.
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15