summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKimplul <kimi.h.kuparinen@gmail.com>2026-07-22 22:40:37 +0300
committerKimplul <kimi.h.kuparinen@gmail.com>2026-07-22 22:51:44 +0300
commita6bec2df944d344f1e38e4be5ea4a3dcd9819158 (patch)
treeb514b0820beec906bc9479c6d7ad9f9827241896
parentdd6314503f02dde966adce06e27105f547300077 (diff)
downloadlark-a6bec2df944d344f1e38e4be5ea4a3dcd9819158.tar.gz
lark-a6bec2df944d344f1e38e4be5ea4a3dcd9819158.zip
write simple kv-parser for headers
-rw-r--r--src/client.c56
-rw-r--r--src/server.c352
2 files changed, 280 insertions, 128 deletions
diff --git a/src/client.c b/src/client.c
index 5b87c37..45a3c39 100644
--- a/src/client.c
+++ b/src/client.c
@@ -1,7 +1,10 @@
#include <err.h>
#include <string.h>
+#include <errno.h>
#include <stdio.h>
+#include <fcntl.h>
#include <stdlib.h>
+#include <poll.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
@@ -44,29 +47,62 @@ int main()
return -1;
}
- int c;
- while ((c = fgetc(stdin)) != EOF) {
- char cb = c;
- int ret = write(tcp_sock, &cb, 1);
- if (ret == -1) {
- err(EXIT_FAILURE, "failed writing to socket");
+ ret = fcntl(tcp_sock, F_SETFL, O_NONBLOCK);
+ if (ret == -1) {
+ close(tcp_sock);
+ return ret;
+ }
+
+ struct pollfd polls[2] = {{
+ .fd = STDIN_FILENO,
+ .events = POLLIN
+ }, {
+ .fd = tcp_sock,
+ .events = POLLIN
}
+ };
- if (cb == '\n') {
- char buf[4096];
- ret = read(tcp_sock, buf, 4096);
+ while (poll(polls, 2, -1)) {
+ if (polls[0].revents) {
+ char cb = 'a';
+
+ int ret = read(STDIN_FILENO, &cb, 1);
if (ret == -1) {
- printf("Error\n");
+ err(EXIT_FAILURE, "failed reading from stdio");
+ }
+
+ if (ret == 0) {
break;
}
+ ret = write(tcp_sock, &cb, 1);
+ if (ret == -1) {
+ err(EXIT_FAILURE, "failed writing to socket");
+ }
+ }
+
+ if (polls[1].revents) {
+ if (polls[1].revents & POLLHUP) {
+ printf("Closed socket\n");
+ break;
+ }
+
+ char buf[4096];
+ int ret = read(tcp_sock, buf, 4096 - 1);
+ if (ret == -1 && errno != EWOULDBLOCK) {
+ err(EXIT_FAILURE, "failed reading from socket");
+ }
+
if (ret == 0) {
printf("Closed socket\n");
break;
}
+
+ buf[ret] = '\0';
printf("%s\n", buf);
}
}
+
close(tcp_sock);
}
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 <stdlib.h>
#include <string.h>
#include <syslog.h>
+#include <stddef.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <netinet/in.h>
@@ -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;
+ if (!buf)
+ return 0;
- /* default values */
- cmd->start = 0;
- cmd->end = -1;
- cmd->thread = NULL;
+ char *start = buf;
+ while (1) {
+ char *keystart = start;
+ /* skip leading whitespace */
+ while (*keystart && isblank(*keystart))
+ keystart++;
- size_t i = 0;
+ /* end of string, nothing more to read */
+ if (!*keystart)
+ return 0;
- size_t start = 0, split = 0, end = 0;
- for (i = 0; i < max; ++i) {
- if (buf[i] == '=') {
- split = i;
+ /* 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++;
- buf[i] = '\0';
+ /* key ends a line, illegal */
+ if (!*keyend)
+ return -1;
- /* we have something like STRING or STRING=STRING, possibly with
- * escaped characters */
- end = i;
+ /* key contains illegal characters, as we didn't end up on empty
+ * space or the '=' key */
+ if (!(isblank(*keyend) || *keyend == '='))
+ return -1;
- /* special case, consequtive spaces */
- if (end == start) {
- start = i + 1;
- split = start;
- continue;
- }
+ /* next, find out where value starts */
+ char *valstart = keyend;
- if (split == start) {
- /* we're just a single key, like STRING */
- const char *key = &buf[start];
- size_t keylen = end - start;
+ /* skip forward until '=' sign */
+ while (*valstart && *valstart != '=')
+ valstart++;
- 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;
- }
+ /* plain key on a line, illegal */
+ if (!*valstart)
+ return -1;
- } else {
- /* we're something like STRING=STRING */
- const char *key = &buf[start];
- size_t keylen = split - start;
+ /* skip over '=' sign */
+ valstart++;
- const char *value = &buf[split + 1];
- size_t valuelen = end - (split + 1);
+ /* skip forward until actual start of value, when blanks stop */
+ while (*valstart && isblank(*valstart))
+ valstart++;
- /* 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;
- }
+ /* 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;
- } 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;
- }
+ /* skip escape characters */
+ if (*runner == '\\')
+ runner++;
- } else if (strneq(key, "THREAD", keylen)) {
- cmd->thread = strndup(value, valuelen);
+ /* but don't skip end of line of end of string */
+ if (!*runner || *runner == '\n')
+ break;
+
+ runner++;
+ }
- /** @todo check that the string is a valid
- * thread reference, name@server */
+ /* prepare start of next line */
+ while (*runner && *runner != '\n')
+ runner++;
- } else {
- syslog(LOG_DEBUG, "illegal keyvalue: %s\n", key);
- return -1;
+ if (*runner == '\n')
+ runner++;
+
+ start = runner;
+
+ /* 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();
}