cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
orm_socket.c
Go to the documentation of this file.
1
22#ifndef _GNU_SOURCE
23#define _GNU_SOURCE 1
24#endif
26
27#include <stdio.h>
28#include <stdlib.h>
29#include <string.h>
30#include <unistd.h>
31#include <errno.h>
32#include <fcntl.h>
33#include <pthread.h>
34#include <sys/socket.h>
35#include <sys/un.h>
36#include <arpa/inet.h>
37#include <stdint.h>
38
39#include <sqlite3.h>
40#include <cjson/cJSON.h>
42
43/* ------------------------------------------------------------------------- */
44/* Protocol constants */
45/* ------------------------------------------------------------------------- */
46
48#define ORM_PROTO_MAGIC 0x43574F52 /* "CWOR" in ASCII hex trickery */
49
50/* ------------------------------------------------------------------------- */
51/* Worker context */
52/* ------------------------------------------------------------------------- */
53
57typedef struct {
58 int fd;
59 char *db_path;
60} cwist_orm_worker_ctx_t;
61
62/* ------------------------------------------------------------------------- */
63/* Helpers */
64/* ------------------------------------------------------------------------- */
65
70static int recv_all(int fd, void *buf, size_t n)
71{
72 size_t total = 0;
73 char *p = (char *)buf;
74 while (total < n) {
75 ssize_t r = recv(fd, p + total, n - total, MSG_WAITALL);
76 if (r <= 0) return -1;
77 total += (size_t)r;
78 }
79 return 0;
80}
81
86static int send_all(int fd, const void *buf, size_t n)
87{
88 size_t total = 0;
89 const char *p = (const char *)buf;
90 while (total < n) {
91 ssize_t w = send(fd, p + total, n - total, 0);
92 if (w <= 0) return -1;
93 total += (size_t)w;
94 }
95 return 0;
96}
97
101static void free_worker_ctx(cwist_orm_worker_ctx_t *ctx)
102{
103 /* The context is allocated by the caller's thread and freed by the
104 * detached worker, so it stays on the raw allocator: a cwist_alloc
105 * scope sweep on the caller side must not reclaim it. */
106 if (!ctx) return;
107 free(ctx->db_path);
108 free(ctx);
109}
110
111/* ------------------------------------------------------------------------- */
112/* SQLite statement execution */
113/* ------------------------------------------------------------------------- */
114
122static void socket_add_legacy_cell(cJSON *row, sqlite3_stmt *stmt, int i) {
123 const char *txt = (const char *)sqlite3_column_text(stmt, i);
124 const char *colname = sqlite3_column_name(stmt, i);
125 if (!colname) colname = "";
126
127 if (!txt) {
129 return;
130 }
131
132 /* Attempt numeric conversion for cleaner JSON. */
133 char *endptr = NULL;
134 errno = 0;
135 long long ll = strtoll(txt, &endptr, 10);
136 char canon[32];
137 if (endptr && *endptr == '\0' && errno == 0) {
138 /* Emit the digits as raw JSON: cJSON prints numbers with 15
139 * significant digits, which corrupted integers from ~1e15 up.
140 * Only canonical integer text becomes a number; "007" or
141 * " 12" is text that merely parses as one and stays a string. */
142 snprintf(canon, sizeof(canon), "%lld", ll);
143 if (strcmp(canon, txt) == 0) {
145 } else {
147 }
148 } else {
149 double d = strtod(txt, &endptr);
150 if (endptr && *endptr == '\0' && errno == 0) {
152 } else {
154 }
155 }
156}
157
159static const char socket_b64_table[] =
160 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
161
166static cJSON *socket_b64_string(const unsigned char *data, int n) {
167 size_t out_cap = ((size_t)n / 3 + 1) * 4 + 1;
168 char *out = (char *)malloc(out_cap);
169 if (!out) return NULL;
170 size_t o = 0;
171 int i = 0;
172 for (; i + 3 <= n; i += 3) {
173 unsigned v = ((unsigned)data[i] << 16) | ((unsigned)data[i + 1] << 8) | data[i + 2];
174 out[o++] = socket_b64_table[(v >> 18) & 63];
175 out[o++] = socket_b64_table[(v >> 12) & 63];
176 out[o++] = socket_b64_table[(v >> 6) & 63];
177 out[o++] = socket_b64_table[v & 63];
178 }
179 int rem = n - i;
180 if (rem == 1) {
181 unsigned v = (unsigned)data[i] << 16;
182 out[o++] = socket_b64_table[(v >> 18) & 63];
183 out[o++] = socket_b64_table[(v >> 12) & 63];
184 out[o++] = '=';
185 out[o++] = '=';
186 } else if (rem == 2) {
187 unsigned v = ((unsigned)data[i] << 16) | ((unsigned)data[i + 1] << 8);
188 out[o++] = socket_b64_table[(v >> 18) & 63];
189 out[o++] = socket_b64_table[(v >> 12) & 63];
190 out[o++] = socket_b64_table[(v >> 6) & 63];
191 out[o++] = '=';
192 }
193 out[o] = '\0';
194 cJSON *node = cJSON_CreateString(out);
195 free(out);
196 return node;
197}
198
209static cJSON *socket_typed_cell(sqlite3_stmt *stmt, int i, int ctype) {
211 if (!cell) return NULL;
212
213 /* Read the typed payload before any text conversion: sqlite3_column_text()
214 * changes what sqlite3_column_type() reports for the same column. */
215 switch (ctype) {
216 case SQLITE_INTEGER: {
217 char digits[32];
218 snprintf(digits, sizeof(digits), "%lld", (long long)sqlite3_column_int64(stmt, i));
219 cJSON_AddStringToObject(cell, "t", "int");
221 break;
222 }
223 case SQLITE_FLOAT:
224 cJSON_AddStringToObject(cell, "t", "real");
226 break;
227 case SQLITE_BLOB: {
228 const unsigned char *blob = (const unsigned char *)sqlite3_column_blob(stmt, i);
230 cJSON_AddStringToObject(cell, "t", "blob");
231 cJSON_AddItemToObject(cell, "v", socket_b64_string(blob ? blob : (const unsigned char *)"", n));
232 cJSON_AddNumberToObject(cell, "n", (double)n);
233 break;
234 }
235 case SQLITE_TEXT:
236 cJSON_AddStringToObject(cell, "t", "text");
238 break;
239 case SQLITE_NULL:
240 default:
241 cJSON_AddStringToObject(cell, "t", "null");
242 break;
243 }
244 return cell;
245}
246
250typedef struct {
251 cJSON *rows;
252 cJSON *columns;
253 cJSON *trows;
254} query_accumulator_t;
255
268static int socket_exec_all(sqlite3 *db, const char *sql, query_accumulator_t *acc,
269 char **errmsg) {
270 const char *tail = sql;
271 int rc = SQLITE_OK;
272
273 while (tail && *tail) {
275 rc = sqlite3_prepare_v2(db, tail, -1, &stmt, &tail);
276 if (rc != SQLITE_OK) break;
277 if (!stmt) continue; /* comment or whitespace between statements */
278
280 if (ncols > 0 && acc->columns) {
281 /* Re-shape the typed column list for this statement. */
282 cJSON_Delete(acc->columns);
283 acc->columns = cJSON_CreateArray();
284 if (acc->columns) {
285 for (int i = 0; i < ncols; i++) {
286 const char *name = sqlite3_column_name(stmt, i);
287 cJSON_AddItemToArray(acc->columns, cJSON_CreateString(name ? name : ""));
288 }
289 }
290 }
291
292 while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) {
295 if (!row || !trow) {
299 break;
300 }
301 for (int i = 0; i < ncols; i++) {
303 cJSON_AddItemToArray(trow, socket_typed_cell(stmt, i, ctype));
304 socket_add_legacy_cell(row, stmt, i);
305 }
308 }
309
311 if (rc == SQLITE_DONE && frc != SQLITE_OK) rc = frc;
312 if (rc != SQLITE_DONE) break;
313 rc = SQLITE_OK;
314 }
315
316 if (rc != SQLITE_OK && errmsg) *errmsg = strdup(sqlite3_errmsg(db));
317 return rc;
318}
319
320/* ------------------------------------------------------------------------- */
321/* Worker main loop */
322/* ------------------------------------------------------------------------- */
323
330static void *cwist_orm_socket_worker(void *arg)
331{
332 cwist_orm_worker_ctx_t *ctx = (cwist_orm_worker_ctx_t *)arg;
333 if (!ctx) return NULL;
334
335 sqlite3 *db = NULL;
336 int rc = sqlite3_open(ctx->db_path, &db);
337 if (rc != SQLITE_OK) {
338 fprintf(stderr, "[ORM-SOCKET] sqlite3_open failed: %s\n",
339 sqlite3_errmsg(db));
340 sqlite3_close(db);
341 close(ctx->fd);
342 free_worker_ctx(ctx);
343 return NULL;
344 }
345
346 /* Speed up concurrent access semantics for single-writer scenarios. */
347 sqlite3_exec(db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
348
349 for (;;) {
350 /* ---- read request length (4 bytes, big-endian) ---- */
351 uint32_t net_len = 0;
352 if (recv_all(ctx->fd, &net_len, sizeof(net_len)) != 0) break;
354 if (sql_len == 0 || sql_len > 16 * 1024 * 1024) break; /* sanity */
355
356 /* ---- read SQL text ---- */
357 char *sql = (char *)cwist_alloc(sql_len + 1);
358 if (!sql) break;
359 if (recv_all(ctx->fd, sql, sql_len) != 0) {
361 break;
362 }
363 sql[sql_len] = '\0';
364
365 /* ---- execute ----
366 * sql comes from the in-process peer, not from an external client;
367 * see the trust boundary note at the top of this file. */
368 query_accumulator_t acc;
369 acc.rows = cJSON_CreateArray();
370 acc.columns = cJSON_CreateArray();
371 acc.trows = cJSON_CreateArray();
372 char *errmsg = NULL;
373
374 if (!acc.rows || !acc.columns || !acc.trows) {
376 } else {
377 rc = socket_exec_all(db, sql, &acc, &errmsg);
378 }
380
381 /* ---- build response payload ---- */
383 if (!resp) {
384 cJSON_Delete(acc.rows);
385 cJSON_Delete(acc.columns);
386 cJSON_Delete(acc.trows);
388 break;
389 }
390
391 cJSON_AddNumberToObject(resp, "status", (double)rc);
392 if (errmsg) {
394 free(errmsg);
395 } else {
396 cJSON_AddNullToObject(resp, "error");
397 }
398 cJSON_AddItemToObject(resp, "rows", acc.rows);
400 if (typed) {
401 cJSON_AddItemToObject(typed, "columns", acc.columns);
402 cJSON_AddItemToObject(typed, "rows", acc.trows);
403 } else {
404 cJSON_Delete(acc.columns);
405 cJSON_Delete(acc.trows);
406 }
407
408 char *payload = cJSON_PrintUnformatted(resp);
410 if (!payload) break;
411
412 uint32_t payload_len = (uint32_t)strlen(payload);
413 if (payload_len > UINT32_MAX - 1) {
414 cJSON_free(payload);
415 break;
416 }
417
418 /* ---- send framed response ---- */
419 uint32_t net_payload = htonl(payload_len);
420 if (send_all(ctx->fd, &net_payload, sizeof(net_payload)) != 0) {
421 cJSON_free(payload);
422 break;
423 }
424 if (send_all(ctx->fd, payload, payload_len) != 0) {
425 cJSON_free(payload);
426 break;
427 }
428 cJSON_free(payload);
429 }
430
431 sqlite3_close(db);
432 close(ctx->fd);
433 free_worker_ctx(ctx);
434 return NULL;
435}
436
437/* ------------------------------------------------------------------------- */
438/* Public API */
439/* ------------------------------------------------------------------------- */
440
441#ifndef SOCK_CLOEXEC
442#define SOCK_CLOEXEC 0
443#endif
444
445static void set_cloexec(int fd) {
446 if (fd >= 0) {
447 int flags = fcntl(fd, F_GETFD);
448 if (flags != -1) {
449 fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
450 }
451 }
452}
453
454int cwist_orm_socketpair_cloexec(int domain, int type, int protocol, int sv[2]) {
455 int res = -1;
456#if defined(SOCK_CLOEXEC) && SOCK_CLOEXEC != 0
457 res = socketpair(domain, type | SOCK_CLOEXEC, protocol, sv);
458#endif
459 if (res != 0) {
460 res = socketpair(domain, type & ~SOCK_CLOEXEC, protocol, sv);
461 if (res == 0) {
462 set_cloexec(sv[0]);
463 set_cloexec(sv[1]);
464 }
465 } else {
466 set_cloexec(sv[0]);
467 set_cloexec(sv[1]);
468 }
469 return res;
470}
471
472
474{
475 if (!db_path) {
476 errno = EINVAL;
477 return -1;
478 }
479
480 int sv[2];
481 /* SOCK_CLOEXEC is not available on every BSD-family system (macOS lacks
482 * it); the helper falls back to fcntl(FD_CLOEXEC) there. */
484 return -1;
485 }
486
487 cwist_orm_worker_ctx_t *ctx =
488 (cwist_orm_worker_ctx_t *)calloc(1, sizeof(*ctx));
489 if (!ctx) {
490 close(sv[0]);
491 close(sv[1]);
492 return -1;
493 }
494
495 ctx->fd = sv[1];
496 ctx->db_path = strdup(db_path);
497 if (!ctx->db_path) {
498 free(ctx);
499 close(sv[0]);
500 close(sv[1]);
501 return -1;
502 }
503
508
509 int rc = pthread_create(&tid, &attr, cwist_orm_socket_worker, ctx);
511
512 if (rc != 0) {
513 free_worker_ctx(ctx);
514 close(sv[0]);
515 close(sv[1]);
516 errno = rc;
517 return -1;
518 }
519
520 return sv[0]; /* caller owns this end */
521}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
int cwist_db_transfer_sqlite_to_socket(const char *db_path)
Open a SQLite database behind an in-process SQL socket.
Definition orm_socket.c:473
int cwist_orm_socketpair_cloexec(int domain, int type, int protocol, int sv[2])
Definition orm_socket.c:454
#define SOCK_CLOEXEC
Definition orm_socket.c:442
size_t cap
Definition otel.c:56