summaryrefslogtreecommitdiff
path: root/src/proto.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/proto.c')
-rw-r--r--src/proto.c183
1 files changed, 183 insertions, 0 deletions
diff --git a/src/proto.c b/src/proto.c
new file mode 100644
index 0000000..6c608d3
--- /dev/null
+++ b/src/proto.c
@@ -0,0 +1,183 @@
+#include "proto.h"
+#include "umbrella.h"
+#include "log.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <arpa/inet.h>
+
+/* ── Low-level I/O ───────────────────────────────────────────────────────── */
+
+/*
+ * write_all: keep writing until all bytes are sent or an error occurs.
+ * Returns 0 on success, -1 on error.
+ */
+static int write_all(int fd, const void *buf, size_t len) {
+ const uint8_t *p = buf;
+ while (len > 0) {
+ ssize_t n = write(fd, p, len);
+ if (n < 0) {
+ if (errno == EINTR) continue;
+ return -1;
+ }
+ p += n;
+ len -= n;
+ }
+ return 0;
+}
+
+/*
+ * read_all: keep reading until exactly len bytes are received or error.
+ * Returns 0 on success, -1 on error, 1 on clean EOF.
+ */
+static int read_all(int fd, void *buf, size_t len) {
+ uint8_t *p = buf;
+ while (len > 0) {
+ ssize_t n = read(fd, p, len);
+ if (n < 0) {
+ if (errno == EINTR) continue;
+ return -1;
+ }
+ if (n == 0)
+ return 1; /* clean disconnect */
+ p += n;
+ len -= n;
+ }
+ return 0;
+}
+
+/* ── Protocol ────────────────────────────────────────────────────────────── */
+
+int proto_send(int fd, const char *json) {
+ uint32_t len = (uint32_t)strlen(json);
+ uint32_t net_len = htonl(len); /* big-endian on the wire */
+
+ if (write_all(fd, &net_len, 4) != 0) {
+ log_debug("proto_send: write header failed: %s", strerror(errno));
+ return -1;
+ }
+ if (write_all(fd, json, len) != 0) {
+ log_debug("proto_send: write body failed: %s", strerror(errno));
+ return -1;
+ }
+ return 0;
+}
+
+int proto_recv(int fd, char *buf, int buf_size) {
+ uint32_t net_len;
+ int r = read_all(fd, &net_len, 4);
+ if (r != 0) return r == 1 ? 0 : -1; /* 0=disconnect, -1=error */
+
+ uint32_t len = ntohl(net_len);
+ if (len == 0 || (int)len >= buf_size) {
+ log_warn("proto_recv: bad message length %u", len);
+ return -1;
+ }
+
+ r = read_all(fd, buf, len);
+ if (r != 0) return r == 1 ? 0 : -1;
+
+ buf[len] = '\0';
+ return (int)len;
+}
+
+/* ── Convenience senders ─────────────────────────────────────────────────── */
+
+int proto_send_ok(int fd) {
+ return proto_send(fd, "{\"type\":\"ok\"}");
+}
+
+int proto_send_error(int fd, const char *message) {
+ char buf[512];
+ /* Simple JSON escaping: replace " with \" */
+ char escaped[256];
+ int j = 0;
+ for (int i = 0; message[i] && j < 250; i++) {
+ if (message[i] == '"') escaped[j++] = '\\';
+ escaped[j++] = message[i];
+ }
+ escaped[j] = '\0';
+
+ snprintf(buf, sizeof(buf),
+ "{\"type\":\"error\",\"message\":\"%s\"}", escaped);
+ return proto_send(fd, buf);
+}
+
+int proto_send_output(int fd, const char *unit_name,
+ const char *data, int history) {
+ /*
+ * We build the JSON manually to avoid a dependency on a JSON library.
+ * The data field needs escaping — we handle the common cases.
+ */
+ char escaped[RING_BUF_LINE_MAX * 2];
+ int j = 0;
+ for (int i = 0; data[i] && j < (int)sizeof(escaped) - 2; i++) {
+ switch (data[i]) {
+ case '"': escaped[j++] = '\\'; escaped[j++] = '"'; break;
+ case '\\': escaped[j++] = '\\'; escaped[j++] = '\\'; break;
+ case '\n': escaped[j++] = '\\'; escaped[j++] = 'n'; break;
+ case '\r': escaped[j++] = '\\'; escaped[j++] = 'r'; break;
+ default: escaped[j++] = data[i]; break;
+ }
+ }
+ escaped[j] = '\0';
+
+ char buf[PROTO_MAX_MSG];
+ snprintf(buf, sizeof(buf),
+ "{\"type\":\"output\",\"unit\":\"%s\",\"data\":\"%s\","
+ "\"history\":%s}",
+ unit_name, escaped, history ? "true" : "false");
+
+ return proto_send(fd, buf);
+}
+
+/* ── Minimal JSON value extractor ────────────────────────────────────────── */
+/*
+ * Finds "key":"value" or "key":value in a flat JSON object.
+ * Not a full JSON parser — sufficient for our simple command messages.
+ */
+int json_get_str(const char *json, const char *key,
+ char *out, int out_size) {
+ char needle[MAX_NAME + 4];
+ snprintf(needle, sizeof(needle), "\"%s\":", key);
+
+ const char *p = strstr(json, needle);
+ if (!p) return 0;
+
+ p += strlen(needle);
+
+ /* Skip whitespace */
+ while (*p == ' ' || *p == '\t') p++;
+
+ if (*p == '"') {
+ /* Quoted string */
+ p++;
+ int i = 0;
+ while (*p && *p != '"' && i < out_size - 1) {
+ if (*p == '\\' && *(p+1)) {
+ p++;
+ switch (*p) {
+ case 'n': out[i++] = '\n'; break;
+ case 'r': out[i++] = '\r'; break;
+ case 't': out[i++] = '\t'; break;
+ default: out[i++] = *p; break;
+ }
+ } else {
+ out[i++] = *p;
+ }
+ p++;
+ }
+ out[i] = '\0';
+ return 1;
+ } else {
+ /* Unquoted value (number, bool) — read until delimiter */
+ int i = 0;
+ while (*p && *p != ',' && *p != '}' && *p != ' ' && i < out_size - 1)
+ out[i++] = *p++;
+ out[i] = '\0';
+ return i > 0 ? 1 : 0;
+ }
+}