From a6bec2df944d344f1e38e4be5ea4a3dcd9819158 Mon Sep 17 00:00:00 2001 From: Kimplul Date: Wed, 22 Jul 2026 22:40:37 +0300 Subject: write simple kv-parser for headers --- src/server.c | 366 +++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 241 insertions(+), 125 deletions(-) (limited to 'src/server.c') diff --git a/src/server.c b/src/server.c index 187e600..2fae757 100644 --- a/src/server.c +++ b/src/server.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,7 @@ enum que_op { struct meta_parse_cmd {}; enum sync_state { - SYNC_INIT = 0, + SYNC_HEADER = 0, SYNC_SEND }; @@ -52,7 +53,7 @@ struct server_conn { socklen_t addrlen; struct strbuf strbuf; - size_t headerlen; + size_t nextlen; enum server_cmd cmd; union { @@ -122,6 +123,11 @@ static bool strneq(const char *s1, const char *s2, size_t n) return strncmp(s1, s2, n) == 0; } +static bool streq(const char *s1, const char *s2) +{ + return strcmp(s1, s2) == 0; +} + static int new_connection(struct server_ctx *ctx, int new_conn_fd) { struct sockaddr addr; @@ -158,7 +164,7 @@ static int new_connection(struct server_ctx *ctx, int new_conn_fd) .addrlen = addrlen, .cmd = META_PARSE_CMD, .strbuf = strbuf_create(0), - .headerlen = 0 + .nextlen = 0 }; memset(&conn.u, 0, sizeof(conn.u)); @@ -186,47 +192,76 @@ static int kill_connection(struct server_ctx *ctx, struct server_conn *conn) return 0; } -/* re-entrant read, returns < 0 on error, 0 on not enough data and > 0 showing the - * length of the cmd (including newline) */ -static ssize_t reread_cmd(struct server_conn *conn, size_t max) +/* re-entrant read, returns < 0 on error, > 0 for how + * large the buffer is currently */ +static ssize_t reread(struct server_conn *conn, size_t c) { + assert(c != 0); + struct strbuf *strbuf = &conn->strbuf; - /* by default, read up to a page of data at a time */ size_t n = strbuf->n; + strbuf_reserve(strbuf, n + c); - /* grow to hold up to 4096 new chars */ - strbuf_reserve(strbuf, n + 4096); + ssize_t r = readx(conn->fd, strbuf->buf + n, c); + if (r < 0) + return r; - /* read up to 4096 new chars */ - ssize_t r = readx(conn->fd, strbuf->buf + n, 4096); - if (r == 0) { - /* shrink back to original size and try again later */ - strbuf_shrink(strbuf, n); - return 0; - } + strbuf_shrink(strbuf, n + r); + return n + r; +} - /* some unforeseen error occured */ +/* read a full command, using at most max bytes. + * < 0 on error (including no command found in the allotted byte count), + * 0 if no command found yet, + * > 0 when command is available, indicating command length (including newline) + * + * Note that this does not respect timeouts, and each command that uses this + * should keep track of its own timing. + */ +static ssize_t reread_cmd(struct server_conn *conn, size_t max) +{ + struct strbuf *strbuf = &conn->strbuf; + + /* previous occupancy */ + size_t n = strbuf->n; + ssize_t r = reread(conn, max); if (r < 0) return r; - /* set to actual size */ - strbuf_shrink(strbuf, n + r); + if (r == (ssize_t)n) + return 0; - /* look for newline */ - for (ssize_t i = 0; i < r; ++i) { - /* read bytes go above allowed limit */ - if (n + i > max) + /* parse the new bytes and see if we have an end-of-command marker */ + char *buf = strbuf->buf; + for (ssize_t i = n; i < r; ++i) { + if (i > (ssize_t)max) return -1; - if (strbuf->buf[n + i] == '\n') - return n + i + 1; + if (buf[i] == '\n') + return i + 1; } - /* no command as of yet, but try again later */ return 0; } +/* read up to n bytes. + * < 0 on error + * 0 if not enough bytes yet + * > 0 to indicate how many bytes are available (should always be n) + */ +static ssize_t reread_n(struct server_conn *conn, size_t n) +{ + ssize_t r = reread(conn, n); + if (r < 0) + return r; + + if (r < (ssize_t)n) + return 0; + + return r; +} + /* set timeout to new smallest value, taking infinite timeout (-1) into account. * */ static void set_timeout(struct server_ctx *ctx, int timeout) @@ -318,7 +353,7 @@ static void strbuf_shift(struct strbuf *strbuf, size_t i) strbuf_shrink(strbuf, strbuf->n - i); } -static enum que_op meta_parse_cmd(struct server_ctx *ctx, struct server_conn *conn) +static enum que_op meta_parse_cmd(struct server_conn *conn) { int r = reread_cmd(conn, MAX_CMD_LEN); if (r < 0) { @@ -333,11 +368,13 @@ static enum que_op meta_parse_cmd(struct server_ctx *ctx, struct server_conn *co return LARK_POLL; } - /* r > 0, we (should) have a command and its headerlen */ + /* r > 0, we (should) have a command and its nextlen */ char *cmd = NULL; conn->strbuf.buf[r - 1] = '\0'; - if (parse_cmd(conn->strbuf.buf, &cmd, &conn->headerlen)) + if (parse_cmd(conn->strbuf.buf, &cmd, &conn->nextlen)) { + syslog(LOG_DEBUG, "invalid command %s, killing connection\n", conn->strbuf.buf); return LARK_KILL; + } if (strneq(conn->strbuf.buf, "SYNC", 4)) { syslog(LOG_DEBUG, "parsed SYNC command start\n"); @@ -350,109 +387,131 @@ static enum que_op meta_parse_cmd(struct server_ctx *ctx, struct server_conn *co return LARK_KILL; } -static int sync_parse(struct sync_cmd *cmd, struct strbuf *strbuf) +/* could also take the length of the buf? */ +typedef int (*kv_cb_t)(char *key, char *value, void *data); +static int parse_kv(char *buf, int (*kv_cb)(char *key, char *value, void *data), void *data) { - char *buf = strbuf->buf; - size_t max = strbuf->n; - - /* default values */ - cmd->start = 0; - cmd->end = -1; - cmd->thread = NULL; - - size_t i = 0; + if (!buf) + return 0; - size_t start = 0, split = 0, end = 0; - for (i = 0; i < max; ++i) { - if (buf[i] == '=') { - split = i; + char *start = buf; + while (1) { + char *keystart = start; + /* skip leading whitespace */ + while (*keystart && isblank(*keystart)) + keystart++; + + /* end of string, nothing more to read */ + if (!*keystart) + return 0; + + /* end of blank line, skip to next */ + if (*keystart == '\n') { + start++; continue; } - if (buf[i] == '\\') { - /* skip escaped character */ - ++i; + /* start of empty line just a comment, skip to next line if one exists */ + if (*keystart == '#') { + while (*keystart && *keystart != '\n') + keystart++; + + /* end of string */ + if (!*keystart) + return 0; + + /* skip line-ending '\n' */ + start = keystart + 1; continue; } - /* regular characters, don't do anything special with them */ - if (buf[i] != ' ' && buf[i] != '\n') - continue; + /* start now points at the first character of a key. Next, parse + * key forward a bit to check that all characters are valid */ + char *keyend = keystart; + while (*keyend && (isalnum(*keyend) || *keyend == '-' || *keyend == '_')) + keyend++; + + /* key ends a line, illegal */ + if (!*keyend) + return -1; - buf[i] = '\0'; + /* key contains illegal characters, as we didn't end up on empty + * space or the '=' key */ + if (!(isblank(*keyend) || *keyend == '=')) + return -1; - /* we have something like STRING or STRING=STRING, possibly with - * escaped characters */ - end = i; + /* next, find out where value starts */ + char *valstart = keyend; - /* special case, consequtive spaces */ - if (end == start) { - start = i + 1; - split = start; - continue; + /* skip forward until '=' sign */ + while (*valstart && *valstart != '=') + valstart++; + + /* plain key on a line, illegal */ + if (!*valstart) + return -1; + + /* skip over '=' sign */ + valstart++; + + /* skip forward until actual start of value, when blanks stop */ + while (*valstart && isblank(*valstart)) + valstart++; + + /* find end of value. Note that empty values are allowed, + * whether they're empty by end of string or end of line or the + * start of a comment*/ + char *valend = valstart; + char *runner = valend; + while (*runner && *runner != '#' && *runner != '\n') { + /* keep track of where last non-whitespace character is + * since it tells us where the value ends */ + if (!isblank(*runner)) + valend = runner + 1; + + /* skip escape characters */ + if (*runner == '\\') + runner++; + + /* but don't skip end of line of end of string */ + if (!*runner || *runner == '\n') + break; + + runner++; } - if (split == start) { - /* we're just a single key, like STRING */ - const char *key = &buf[start]; - size_t keylen = end - start; + /* prepare start of next line */ + while (*runner && *runner != '\n') + runner++; - if (strneq(key, "SYNC", keylen)) { - /* nothing to do, which technically speaking - * allows the command name to be repeated arbitratily often */ - } - else { - syslog(LOG_DEBUG, "illegal flag name: %s\n", key); - return -1; - } + if (*runner == '\n') + runner++; + + start = runner; - } else { - /* we're something like STRING=STRING */ - const char *key = &buf[start]; - size_t keylen = split - start; - - const char *value = &buf[split + 1]; - size_t valuelen = end - (split + 1); - - /* this seems fairly easy to automate, just pass in an - * array of structs that describe the name and type of - * each kv pair, the user can then make more checks - * after parsing */ - if (strneq(key, "START", keylen)) { - char *endptr = NULL; - cmd->start = strtol(value, &endptr, 10); - if (value + valuelen != endptr) { - syslog(LOG_DEBUG, "not an integer: %s\n", key); - return -1; - } - - } else if (strneq(key, "END", keylen)) { - char *endptr = NULL; - cmd->start = strtol(value, &endptr, 10); - if (value + valuelen != endptr) { - syslog(LOG_DEBUG, "not an integer: %s\n", key); - return -1; - } - - } else if (strneq(key, "THREAD", keylen)) { - cmd->thread = strndup(value, valuelen); - - /** @todo check that the string is a valid - * thread reference, name@server */ - - } else { - syslog(LOG_DEBUG, "illegal keyvalue: %s\n", key); - return -1; + /* present parsed values to user */ + *keyend = '\0'; + *valend = '\0'; + + /* unescape backslashes */ + long shift = 0; + for (ptrdiff_t i = 0; i < valend - valstart + 1; ++i) { + if (valstart[i] == '\\') { + shift++; + continue; } - } + if (shift == 0) + continue; + + valstart[i - shift] = valstart[i]; + } - /* next iter */ - start = i + 1; - split = start; + int r = kv_cb(keystart, valstart, data); + if (r) + return r; } - strbuf_shift(strbuf, i); return 0; } @@ -461,30 +520,87 @@ static void destroy_sync(struct sync_cmd *cmd) free(cmd->thread); } +static int sync_kv(char *key, char *value, struct sync_cmd *cmd) +{ + if (streq(key, "START")) { + char *end = NULL; + cmd->start = strtol(value, &end, 0); + if (end == value || *end != '\0') { + syslog(LOG_DEBUG, "not an integer value: %s = %s", key, value); + return -1; + } + + if (cmd->start < 0) { + syslog(LOG_DEBUG, "illegal integer value: %ld", cmd->start); + return -1; + } + + return 0; + } + + if (streq(key, "END")) { + char *end = NULL; + cmd->end = strtol(value, &end, 0); + if (end == value || *end != '\0') { + syslog(LOG_DEBUG, "not an integer value: %s = %s", key, value); + return -1; + } + + if (cmd->end < 0 && cmd->end != -1) { + syslog(LOG_DEBUG, "illegal integer value: %ld", cmd->end); + return -1; + } + + return 0; + } + + if (streq(key, "THREAD")) { + /** @todo check validness, copy default value, etc. */ + cmd->thread = strdup(value); + return 0; + } + + syslog(LOG_DEBUG, "unknown parameter to sync: %s", key); + return -1; +} + static enum que_op sync_cmd(struct server_ctx *ctx, struct server_conn *conn) { struct sync_cmd *cmd = &conn->u.sync; switch (cmd->state) { - case SYNC_INIT: { - if (sync_parse(cmd, &conn->strbuf)) - return LARK_KILL; - - if (cmd->start < 0) { - syslog(LOG_DEBUG, "illegal value: SYNC START=%ld\n", cmd->start); - destroy_sync(cmd); + case SYNC_HEADER: { + ssize_t r = reread_n(conn, conn->nextlen); + if (r < 0) { + syslog(LOG_DEBUG, "client messed up connection"); return LARK_KILL; } - if (cmd->end < 0 && cmd->end != -1) { - syslog(LOG_DEBUG, "illegal value: SYNC END=%ld\n", cmd->end); + /** @todo respect timeout of some kind? */ + if (r == 0) + return LARK_POLL; + + /* headr must include trailing newline, so we can be sure that a + * valid header will remain valid if we null-terminate the + * header */ + conn->strbuf.buf[r - 1] = '\0'; + + /* parse ini-style header */ + + /* some default values, arguably not sensible to always sync a + * whole thread but eh */ + cmd->start = 0; + cmd->end = -1; + cmd->thread = NULL; + + if (parse_kv(conn->strbuf.buf, (kv_cb_t)sync_kv, cmd)) { + syslog(LOG_DEBUG, "failed parsing SYNC header"); destroy_sync(cmd); return LARK_KILL; } - if (cmd->thread == NULL) { - /** @todo copy over our home thread if it exists */ - } + strbuf_shift(&conn->strbuf, r); + syslog(LOG_DEBUG, "got valid sync header\n"); cmd->state = SYNC_SEND; return sync_cmd(ctx, conn); } @@ -578,7 +694,7 @@ static int event_loop(int tcp_sock) enum que_op op = LARK_KILL; switch (conn->cmd) { - case META_PARSE_CMD: op = meta_parse_cmd(&ctx, conn); break; + case META_PARSE_CMD: op = meta_parse_cmd(conn); break; case SYNC_CMD: op = sync_cmd(&ctx, conn); break; default: abort(); } -- cgit v1.3