aboutsummaryrefslogtreecommitdiff
path: root/src/util.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-17 19:55:36 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-17 19:55:36 +0200
commit380195f7cd5e57acf2c1cf2bc41069e6b0b979ed (patch)
tree32a88fb22a7fbe8f1fd5c105156d1f928c93950d /src/util.c
downloadbokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.tar.gz
bokf-380195f7cd5e57acf2c1cf2bc41069e6b0b979ed.zip
Initial commit: daemon, clients, docs, Docker deploy pipelinev0.1.0
Diffstat (limited to 'src/util.c')
-rw-r--r--src/util.c564
1 files changed, 564 insertions, 0 deletions
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..9543875
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,564 @@
+#include "util.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/random.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "sha256.h"
+
+void *xmalloc(size_t n)
+{
+ void *p = malloc(n ? n : 1);
+ if (!p) {
+ perror("malloc");
+ exit(1);
+ }
+ return p;
+}
+
+void *xcalloc(size_t n, size_t sz)
+{
+ void *p = calloc(n ? n : 1, sz ? sz : 1);
+ if (!p) {
+ perror("calloc");
+ exit(1);
+ }
+ return p;
+}
+
+void *xrealloc(void *p, size_t n)
+{
+ void *q = realloc(p, n ? n : 1);
+ if (!q) {
+ perror("realloc");
+ exit(1);
+ }
+ return q;
+}
+
+char *xstrdup(const char *s)
+{
+ char *p = strdup(s);
+ if (!p) {
+ perror("strdup");
+ exit(1);
+ }
+ return p;
+}
+
+void buf_init(struct buf *b)
+{
+ b->p = NULL;
+ b->len = 0;
+ b->cap = 0;
+}
+
+void buf_free(struct buf *b)
+{
+ free(b->p);
+ b->p = NULL;
+ b->len = b->cap = 0;
+}
+
+static void buf_reserve(struct buf *b, size_t extra)
+{
+ if (b->len + extra <= b->cap)
+ return;
+ size_t nc = b->cap ? b->cap : 64;
+ while (nc < b->len + extra)
+ nc *= 2;
+ b->p = xrealloc(b->p, nc);
+ b->cap = nc;
+}
+
+void buf_append(struct buf *b, const void *data, size_t n)
+{
+ if (!n)
+ return;
+ buf_reserve(b, n);
+ memcpy(b->p + b->len, data, n);
+ b->len += n;
+}
+
+void buf_append_u16be(struct buf *b, uint16_t v)
+{
+ unsigned char t[2] = { (unsigned char)(v >> 8), (unsigned char)v };
+ buf_append(b, t, sizeof t);
+}
+
+void buf_append_u32be(struct buf *b, uint32_t v)
+{
+ unsigned char t[4] = { (unsigned char)(v >> 24), (unsigned char)(v >> 16),
+ (unsigned char)(v >> 8), (unsigned char)v };
+ buf_append(b, t, sizeof t);
+}
+
+void buf_append_u64be(struct buf *b, uint64_t v)
+{
+ unsigned char t[8];
+ for (int i = 0; i < 8; i++)
+ t[i] = (unsigned char)(v >> (56 - 8 * i));
+ buf_append(b, t, sizeof t);
+}
+
+void util_sha256(const void *data, size_t n, unsigned char out[32])
+{
+ SHA256_CTX ctx;
+ sha256_init(&ctx);
+ sha256_update(&ctx, (const BYTE *)data, n);
+ sha256_final(&ctx, out);
+}
+
+void util_hex(const unsigned char *in, size_t n, char *out)
+{
+ static const char hex[] = "0123456789abcdef";
+ for (size_t i = 0; i < n; i++) {
+ out[2 * i] = hex[in[i] >> 4];
+ out[2 * i + 1] = hex[in[i] & 15];
+ }
+ out[2 * n] = '\0';
+}
+
+static const char B64URL[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+char *util_b64url(const unsigned char *in, size_t n)
+{
+ size_t olen = ((n + 2) / 3) * 4 + 1;
+ char *out = xmalloc(olen);
+ size_t i = 0, o = 0;
+ while (i + 3 <= n) {
+ uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) |
+ (uint32_t)in[i + 2];
+ out[o++] = B64URL[(v >> 18) & 63];
+ out[o++] = B64URL[(v >> 12) & 63];
+ out[o++] = B64URL[(v >> 6) & 63];
+ out[o++] = B64URL[v & 63];
+ i += 3;
+ }
+ if (i + 1 == n) {
+ uint32_t v = (uint32_t)in[i] << 16;
+ out[o++] = B64URL[(v >> 18) & 63];
+ out[o++] = B64URL[(v >> 12) & 63];
+ } else if (i + 2 == n) {
+ uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8);
+ out[o++] = B64URL[(v >> 18) & 63];
+ out[o++] = B64URL[(v >> 12) & 63];
+ out[o++] = B64URL[(v >> 6) & 63];
+ }
+ out[o] = '\0';
+ return out;
+}
+
+char *util_b64(const unsigned char *in, size_t n)
+{
+ static const char B64[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ size_t olen = ((n + 2) / 3) * 4 + 1;
+ char *out = xmalloc(olen);
+ size_t i = 0, o = 0;
+ while (i + 3 <= n) {
+ uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8) |
+ (uint32_t)in[i + 2];
+ out[o++] = B64[(v >> 18) & 63];
+ out[o++] = B64[(v >> 12) & 63];
+ out[o++] = B64[(v >> 6) & 63];
+ out[o++] = B64[v & 63];
+ i += 3;
+ }
+ if (i + 1 == n) {
+ uint32_t v = (uint32_t)in[i] << 16;
+ out[o++] = B64[(v >> 18) & 63];
+ out[o++] = B64[(v >> 12) & 63];
+ out[o++] = '=';
+ out[o++] = '=';
+ } else if (i + 2 == n) {
+ uint32_t v = ((uint32_t)in[i] << 16) | ((uint32_t)in[i + 1] << 8);
+ out[o++] = B64[(v >> 18) & 63];
+ out[o++] = B64[(v >> 12) & 63];
+ out[o++] = B64[(v >> 6) & 63];
+ out[o++] = '=';
+ }
+ out[o] = '\0';
+ return out;
+}
+
+int util_random(void *out, size_t n)
+{
+ unsigned char *p = out;
+ size_t got = 0;
+ while (got < n) {
+ ssize_t r = getrandom(p + got, n - got, 0);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ break;
+ }
+ got += (size_t)r;
+ }
+ if (got == n)
+ return 0;
+ int fd = open("/dev/urandom", O_RDONLY);
+ if (fd < 0)
+ return -1;
+ got = 0;
+ while (got < n) {
+ ssize_t r = read(fd, p + got, n - got);
+ if (r <= 0) {
+ if (r < 0 && errno == EINTR)
+ continue;
+ close(fd);
+ return -1;
+ }
+ got += (size_t)r;
+ }
+ close(fd);
+ return 0;
+}
+
+char *util_random_id(const char *prefix, size_t nbytes)
+{
+ unsigned char raw[64];
+ if (nbytes > sizeof raw)
+ nbytes = sizeof raw;
+ if (util_random(raw, nbytes) != 0) {
+ fprintf(stderr, "fatal: no entropy source\n");
+ exit(1);
+ }
+ char *b = util_b64url(raw, nbytes);
+ size_t plen = prefix ? strlen(prefix) : 0;
+ char *out = xmalloc(plen + strlen(b) + 1);
+ memcpy(out, prefix ? prefix : "", plen);
+ strcpy(out + plen, b);
+ free(b);
+ return out;
+}
+
+int util_const_eq(const void *a, const void *b, size_t n)
+{
+ const volatile unsigned char *x = a;
+ const volatile unsigned char *y = b;
+ unsigned char d = 0;
+ for (size_t i = 0; i < n; i++)
+ d |= (unsigned char)(x[i] ^ y[i]);
+ return d == 0;
+}
+
+int64_t util_now(void)
+{
+ return (int64_t)time(NULL);
+}
+
+void util_iso8601(int64_t t, char *buf, size_t n)
+{
+ time_t tt = (time_t)t;
+ struct tm tm;
+ gmtime_r(&tt, &tm);
+ strftime(buf, n, "%Y-%m-%dT%H:%M:%SZ", &tm);
+}
+
+char *util_str_trim(char *s)
+{
+ while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
+ s++;
+ size_t len = strlen(s);
+ while (len > 0) {
+ char c = s[len - 1];
+ if (c != ' ' && c != '\t' && c != '\r' && c != '\n')
+ break;
+ s[--len] = '\0';
+ }
+ return s;
+}
+
+static int is_leap(int y)
+{
+ return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
+}
+
+int util_parse_iso_date(const char *s)
+{
+ if (!s || strlen(s) != 10)
+ return 0;
+ if (s[4] != '-' || s[7] != '-')
+ return 0;
+ static const int mdays[] = { 31, 28, 31, 30, 31, 30,
+ 31, 31, 30, 31, 30, 31 };
+ int y = 0, m = 0, d = 0;
+ for (int i = 0; i < 10; i++) {
+ if (i == 4 || i == 7)
+ continue;
+ if (s[i] < '0' || s[i] > '9')
+ return 0;
+ }
+ y = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 +
+ (s[3] - '0');
+ m = (s[5] - '0') * 10 + (s[6] - '0');
+ d = (s[8] - '0') * 10 + (s[9] - '0');
+ if (y < 1900 || y > 2200 || m < 1 || m > 12 || d < 1)
+ return 0;
+ int max = mdays[m - 1] + (m == 2 && is_leap(y) ? 1 : 0);
+ return d <= max;
+}
+
+/* ------------------------------------------------------------------ */
+/* base64 */
+/* ------------------------------------------------------------------ */
+
+static int b64_val(char c)
+{
+ if (c >= 'A' && c <= 'Z')
+ return c - 'A';
+ if (c >= 'a' && c <= 'z')
+ return c - 'a' + 26;
+ if (c >= '0' && c <= '9')
+ return c - '0' + 52;
+ if (c == '+')
+ return 62;
+ if (c == '/')
+ return 63;
+ return -1;
+}
+
+int util_b64_decode(const char *in, size_t in_len, unsigned char **out,
+ size_t *out_len)
+{
+ while (in_len > 0 && (in[in_len - 1] == '\n' || in[in_len - 1] == '\r' ||
+ in[in_len - 1] == ' ' || in[in_len - 1] == '\t'))
+ in_len--;
+ size_t i = 0;
+ while (i < in_len && (in[i] == '\n' || in[i] == '\r' || in[i] == ' ' ||
+ in[i] == '\t'))
+ i++;
+ if (i >= in_len)
+ return -1;
+ size_t cap = (in_len - i) / 4 * 3 + 3;
+ unsigned char *buf = xmalloc(cap);
+ size_t o = 0;
+ int pad = 0;
+ for (; i < in_len; i += 4) {
+ if (i + 1 > in_len - 1)
+ break;
+ int v[4];
+ for (int k = 0; k < 4 && i + (size_t)k < in_len; k++) {
+ char c = in[i + (size_t)k];
+ if (c == '=') {
+ pad++;
+ v[k] = 0;
+ } else {
+ v[k] = b64_val(c);
+ if (v[k] < 0) {
+ free(buf);
+ return -1;
+ }
+ if (pad) {
+ free(buf);
+ return -1;
+ }
+ }
+ }
+ if (pad > 2) {
+ free(buf);
+ return -1;
+ }
+ buf[o++] = (unsigned char)((v[0] << 2) | (v[1] >> 4));
+ if (pad < 2)
+ buf[o++] = (unsigned char)(((v[1] & 15) << 4) | (v[2] >> 2));
+ if (pad < 1)
+ buf[o++] = (unsigned char)(((v[2] & 3) << 6) | v[3]);
+ }
+ *out = buf;
+ *out_len = o;
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* dates */
+/* ------------------------------------------------------------------ */
+
+int util_date_parse(const char *s, int *y, int *m, int *d)
+{
+ if (!util_parse_iso_date(s))
+ return -1;
+ *y = (s[0] - '0') * 1000 + (s[1] - '0') * 100 + (s[2] - '0') * 10 +
+ (s[3] - '0');
+ *m = (s[5] - '0') * 10 + (s[6] - '0');
+ *d = (s[8] - '0') * 10 + (s[9] - '0');
+ return 0;
+}
+
+void util_date_fmt(int y, int m, int d, char *buf, size_t n)
+{
+ snprintf(buf, n, "%04d-%02d-%02d", y, m, d);
+}
+
+int util_days_in_month(int y, int m)
+{
+ static const int mdays[] = { 31, 28, 31, 30, 31, 30,
+ 31, 31, 30, 31, 30, 31 };
+ if (m < 1 || m > 12)
+ return 0;
+ return mdays[m - 1] + (m == 2 && is_leap(y) ? 1 : 0);
+}
+
+void util_date_add_months(const char *date, int months, char *out, size_t n)
+{
+ int y, m, d;
+ if (util_date_parse(date, &y, &m, &d) != 0) {
+ snprintf(out, n, "%s", date);
+ return;
+ }
+ int total = y * 12 + (m - 1) + months;
+ int ny = total / 12;
+ int nm = total % 12 + 1;
+ int max = util_days_in_month(ny, nm);
+ if (d > max)
+ d = max;
+ util_date_fmt(ny, nm, d, out, n);
+}
+
+static int64_t days_from_civil(int y, int m, int d)
+{
+ y -= m <= 2;
+ int64_t era = (y >= 0 ? y : y - 399) / 400;
+ unsigned yoe = (unsigned)(y - era * 400);
+ unsigned doy = (unsigned)((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1);
+ unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
+ return era * 146097 + (int64_t)doe - 719468;
+}
+
+static void civil_from_days(int64_t z, int *y, int *m, int *d)
+{
+ z += 719468;
+ int64_t era = (z >= 0 ? z : z - 146096) / 146097;
+ unsigned doe = (unsigned)(z - era * 146097);
+ unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
+ int yy = (int)(yoe + era * 400);
+ unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
+ unsigned mp = (5 * doy + 2) / 153;
+ *d = (int)(doy - (153 * mp + 2) / 5 + 1);
+ *m = (int)(mp + (mp < 10 ? 3 : -9));
+ *y = yy + (*m <= 2);
+}
+
+void util_date_add_days(const char *date, int days, char *out, size_t n)
+{
+ int y, m, d;
+ if (util_date_parse(date, &y, &m, &d) != 0) {
+ snprintf(out, n, "%s", date);
+ return;
+ }
+ int64_t z = days_from_civil(y, m, d) + days;
+ civil_from_days(z, &y, &m, &d);
+ util_date_fmt(y, m, d, out, n);
+}
+
+int util_date_valid(const char *s)
+{
+ return util_parse_iso_date(s);
+}
+
+/* ------------------------------------------------------------------ */
+/* CP437 conversion (subset: Latin-1 letters used in SIE files) */
+/* ------------------------------------------------------------------ */
+
+/* Unicode codepoints for CP437 bytes 0x80..0xDF. Bytes 0xE0..0xFF are
+ Greek/math symbols that do not occur in Swedish bookkeeping text and
+ convert to '?'. */
+static const uint16_t CP437_HIGH[0x60] = {
+ 0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x00E5, 0x00E7,
+ 0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x00EC, 0x00C4, 0x00C5,
+ 0x00C9, 0x00E6, 0x00C6, 0x00F4, 0x00F6, 0x00F2, 0x00FB, 0x00F9,
+ 0x00FF, 0x00D6, 0x00DC, 0x00A2, 0x00A3, 0x00A5, 0x20A7, 0x0192,
+ 0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x00F1, 0x00D1, 0x00AA, 0x00BA,
+ 0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB,
+ 0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
+ 0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
+ 0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
+ 0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
+ 0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
+ 0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
+};
+
+static size_t cp437_put_utf8(unsigned char *out, size_t o, uint16_t cp)
+{
+ if (cp < 0x80) {
+ out[o++] = (unsigned char)cp;
+ } else if (cp < 0x800) {
+ out[o++] = (unsigned char)(0xC0 | (cp >> 6));
+ out[o++] = (unsigned char)(0x80 | (cp & 0x3F));
+ } else {
+ out[o++] = (unsigned char)(0xE0 | (cp >> 12));
+ out[o++] = (unsigned char)(0x80 | ((cp >> 6) & 0x3F));
+ out[o++] = (unsigned char)(0x80 | (cp & 0x3F));
+ }
+ return o;
+}
+
+char *util_cp437_to_utf8(const unsigned char *in, size_t n)
+{
+ char *out = xmalloc(n * 2 + 1);
+ size_t o = 0;
+ for (size_t i = 0; i < n; i++) {
+ unsigned char b = in[i];
+ if (b < 0x80) {
+ out[o++] = (char)b;
+ } else if (b <= 0xDF) {
+ o = cp437_put_utf8((unsigned char *)out, o, CP437_HIGH[b - 0x80]);
+ } else {
+ out[o++] = '?';
+ }
+ }
+ out[o] = '\0';
+ return out;
+}
+
+size_t util_utf8_to_cp437(const char *in, unsigned char *out, size_t out_sz)
+{
+ size_t o = 0;
+ const unsigned char *p = (const unsigned char *)in;
+ while (*p && o < out_sz) {
+ uint32_t cp;
+ size_t len;
+ if (*p < 0x80) {
+ cp = *p;
+ len = 1;
+ } else if ((*p & 0xE0) == 0xC0) {
+ cp = (uint32_t)(*p & 0x1F) << 6;
+ if ((p[1] & 0xC0) != 0x80)
+ break;
+ cp |= p[1] & 0x3F;
+ len = 2;
+ } else if ((*p & 0xF0) == 0xE0) {
+ cp = (uint32_t)(*p & 0x0F) << 12;
+ if ((p[1] & 0xC0) != 0x80 || (p[2] & 0xC0) != 0x80)
+ break;
+ cp |= (uint32_t)(p[1] & 0x3F) << 6;
+ cp |= p[2] & 0x3F;
+ len = 3;
+ } else {
+ p++;
+ out[o++] = '?';
+ continue;
+ }
+ if (cp < 0x80) {
+ out[o++] = (unsigned char)cp;
+ } else {
+ unsigned char best = '?';
+ for (unsigned b = 0x80; b <= 0xDF; b++) {
+ if (CP437_HIGH[b - 0x80] == cp) {
+ best = (unsigned char)b;
+ break;
+ }
+ }
+ out[o++] = best;
+ }
+ p += len;
+ }
+ return o;
+}