summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/arena.c107
-rw-r--r--lib/include/arena.h28
-rw-r--r--lib/include/log.h52
-rw-r--r--lib/include/net.h8
-rw-r--r--lib/include/sha1.h42
-rw-r--r--lib/log.c67
-rw-r--r--lib/macros.h6
-rw-r--r--lib/manifest.c4
-rw-r--r--lib/meson.build5
-rw-r--r--lib/net.c274
-rw-r--r--lib/sha1.c237
11 files changed, 830 insertions, 0 deletions
diff --git a/lib/arena.c b/lib/arena.c
new file mode 100644
index 0000000..bea98ef
--- /dev/null
+++ b/lib/arena.c
@@ -0,0 +1,107 @@
+#include "arena.h"
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <stdio.h>
+
+#ifdef ARENA_ASAN_INSTRUMENT
+#include <sanitizer/asan_interface.h>
+
+#define ASAN_POISON(...) __asan_poison_memory_region(__VA_ARGS__)
+#define ASAN_UNPOISON(...) __asan_unpoison_memory_region(__VA_ARGS__)
+#else
+#define ASAN_POISON(...)
+#define ASAN_UNPOISON(...)
+#endif
+
+struct vl__arena_tag {
+ size_t cap; /* number of bytes after the end of this struct we can use */
+ size_t cur;
+};
+
+#define ARENA_ALIGN (size_t)((1 << 6) - 1)
+#define ROUND_UP(_am) (((_am) + ARENA_ALIGN) & ~ARENA_ALIGN)
+
+static void *arena_addr(void *a, size_t len)
+{
+ return (void *)((uintptr_t)a + ROUND_UP(sizeof(vl_arena)) + len);
+}
+
+/* returns NULL if an arena could not be allocated */
+vl_arena *vl_arena_new(size_t cap)
+{
+ cap = ROUND_UP(cap);
+
+ vl_arena *arena = calloc(1, ROUND_UP(sizeof(vl_arena)) + cap);
+ if (!arena) return NULL;
+
+ arena->cap = cap;
+ arena->cur = 0;
+
+ ASAN_POISON(arena + 1, cap + (ROUND_UP(sizeof(vl_arena)) - sizeof(vl_arena)));
+
+ return arena;
+}
+
+/* aborts if the arena is overflowing */
+void *vl_arena_push(vl_arena *parena, size_t len)
+{
+ size_t rlen = ROUND_UP(len);
+ if (parena->cur + rlen > parena->cap) abort();
+
+ void *ret = arena_addr(parena, parena->cur);
+ parena->cur += rlen;
+
+ ASAN_UNPOISON(ret, len);
+ return ret;
+}
+
+/* resets the arena (but does not free it) */
+void vl_arena_reset(vl_arena *parena)
+{
+ parena->cur = 0;
+ ASAN_POISON(arena_addr(parena, 0), parena->cap);
+}
+
+/* frees the arena */
+void vl_arena_free(vl_arena *parena)
+{
+ free(parena);
+}
+
+char *vl_arena_strdup(vl_arena *parena, const char *str)
+{
+ size_t len = strlen(str);
+ void *out = vl_arena_push(parena, len + 1);
+ memcpy(out, str, len + 1);
+ return out;
+}
+
+char *vl_arena_sprintf(vl_arena *parena, const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+
+ char *ret = vl_arena_vsprintf(parena, fmt, ap);
+
+ va_end(ap);
+ return ret;
+}
+
+char *vl_arena_vsprintf(vl_arena *parena, const char *fmt, va_list ap)
+{
+ va_list copy;
+ va_copy(copy, ap);
+
+ int desire = vsnprintf(NULL, 0, fmt, copy);
+ va_end(copy);
+
+ if (desire < 0) {
+ abort(); /* vsnprintf should never fail */
+ }
+
+ char *str = vl_arena_push(parena, (unsigned)desire + 1);
+ vsnprintf(str, (unsigned)desire + 1, fmt, ap);
+
+ return str;
+}
diff --git a/lib/include/arena.h b/lib/include/arena.h
new file mode 100644
index 0000000..fa1674b
--- /dev/null
+++ b/lib/include/arena.h
@@ -0,0 +1,28 @@
+#ifndef VL_ARENA_H_INCLUDED
+#define VL_ARENA_H_INCLUDED
+
+/* implements a basic arena allocator */
+
+#include <stddef.h>
+#include <stdarg.h>
+
+typedef struct vl__arena_tag vl_arena;
+
+/* returns NULL if an arena could not be allocated */
+vl_arena *vl_arena_new(size_t cap);
+
+/* aborts if the arena is overflowing */
+void *vl_arena_push(vl_arena *parena, size_t len);
+
+/* resets the arena (but does not free it) */
+void vl_arena_reset(vl_arena *parena);
+
+/* frees the arena */
+void vl_arena_free(vl_arena *parena);
+
+char *vl_arena_strdup(vl_arena *parena, const char *str);
+
+char *vl_arena_sprintf(vl_arena *parena, const char *fmt, ...);
+char *vl_arena_vsprintf(vl_arena *parena, const char *fmt, va_list ap);
+
+#endif /* include guard */
diff --git a/lib/include/log.h b/lib/include/log.h
new file mode 100644
index 0000000..a8549e0
--- /dev/null
+++ b/lib/include/log.h
@@ -0,0 +1,52 @@
+#include <stdio.h>
+#include <stdarg.h>
+
+#define LOG_TRACE 0u
+#define LOG_DEBUG 1u
+#define LOG_INFO 2u
+#define LOG_WARN 3u
+#define LOG_ERROR 4u
+
+void vl_log_setlevel(unsigned level);
+
+void vl_logv(unsigned level, const char *fmt, va_list args);
+void vl_log(unsigned level, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+
+#ifndef LOG_MIN_LEVEL
+#define LOG_MIN_LEVEL LOG_TRACE
+#endif
+
+#if LOG_TRACE >= LOG_MIN_LEVEL
+#define LOG_TRACE_ENABLED
+#define vl_trace(...) vl_log(LOG_TRACE, __VA_ARGS__)
+#define vl_tracev(...) vl_logv(LOG_TRACE, __VA_ARGS__)
+#else
+#define vl_trace(...)
+#define vl_tracev(...)
+#endif
+
+#if LOG_DEBUG >= LOG_MIN_LEVEL
+#define LOG_DEBUG_ENABLED
+#define vl_debug(...) vl_log(LOG_DEBUG, __VA_ARGS__)
+#define vl_debugv(...) vl_logv(LOG_DEBUG, __VA_ARGS__)
+#else
+#define vl_debug(...)
+#define vl_debugv(...)
+#endif
+
+#if LOG_INFO >= LOG_MIN_LEVEL
+#define LOG_INFO_ENABLED
+#define vl_info(...) vl_log(LOG_INFO, __VA_ARGS__)
+#define vl_infov(...) vl_logv(LOG_INFO, __VA_ARGS__)
+#else
+#define vl_info(...)
+#define vl_infov(...)
+#endif
+
+#define LOG_WARN_ENABLED
+#define vl_warn(...) vl_log(LOG_WARN, __VA_ARGS__)
+#define vl_warnv(...) vl_logv(LOG_WARN, __VA_ARGS__)
+
+#define LOG_ERROR_ENABLED
+#define vl_error(...) vl_log(LOG_ERROR, __VA_ARGS__)
+#define vl_errorv(...) vl_logv(LOG_ERROR, __VA_ARGS__)
diff --git a/lib/include/net.h b/lib/include/net.h
new file mode 100644
index 0000000..d5f1482
--- /dev/null
+++ b/lib/include/net.h
@@ -0,0 +1,8 @@
+#ifndef VL_NET_H_INCLUDED
+#define VL_NET_H_INCLUDED
+
+#include "arena.h"
+
+int vl_net_ensure_cached(vl_arena *arena, const char *url, const char *target_path);
+
+#endif
diff --git a/lib/include/sha1.h b/lib/include/sha1.h
new file mode 100644
index 0000000..05cedd4
--- /dev/null
+++ b/lib/include/sha1.h
@@ -0,0 +1,42 @@
+#ifndef VL_SHA1_H_INCLUDED
+#define VL_SHA1_H_INCLUDED
+
+#include <stdint.h>
+#include <stddef.h>
+
+#define VL_SHA1_STATELEN_32 ( 5u) /* 160 bits / 32 (bits per int) = 5 ints */
+#define VL_SHA1_CHUNKLEN_BYTES (64u) /* 512 bits / 8 (bits per byte) = 64 bytes */
+
+#define VL_SHA1_DIGEST_BYTES (20u)
+#define VL_SHA1_DIGEST_HEX_STRLEN (VL_SHA1_DIGEST_BYTES << 1)
+
+typedef struct {
+ /* struct fields are internal (struct size is ABI doe) */
+ size_t vl_nchunk;
+ uint64_t vl_total;
+ uint32_t vl_state[VL_SHA1_STATELEN_32];
+ uint8_t vl_chunk[VL_SHA1_CHUNKLEN_BYTES];
+} vl_sha1_st;
+
+typedef uint8_t vl_sha1[VL_SHA1_DIGEST_BYTES];
+
+/* initializes a SHA1 state struct */
+void vl_sha1_init(vl_sha1_st *st);
+
+/* updates a SHA1 state struct with new data (must have been initialized with vl_sha1_init first) */
+void vl_sha1_update(vl_sha1_st *st, const void *data, size_t sz);
+
+/* finalizes a SHA1 state struct, writing the final digest to the second argument. the state struct
+ * is now invalid, and vl_sha1_init must be used on it again before it can be reused. */
+void vl_sha1_finalize(vl_sha1_st *st, vl_sha1 out);
+
+/* shortcut for hashing a buffer */
+void vl_sha1_buffer(vl_sha1 odigest, const void *data, size_t sz);
+
+/* converts digest bytes to a hex string (does NOT place a NUL byte) */
+void vl_sha1_encode(const vl_sha1 dig, char hex[VL_SHA1_DIGEST_HEX_STRLEN]);
+
+/* converts a hex string to digest bytes */
+int vl_sha1_decode(vl_sha1 odigest, const char hex[VL_SHA1_DIGEST_HEX_STRLEN]);
+
+#endif /* include guard */
diff --git a/lib/log.c b/lib/log.c
new file mode 100644
index 0000000..4924782
--- /dev/null
+++ b/lib/log.c
@@ -0,0 +1,67 @@
+#include <string.h>
+#include <time.h>
+#include "log.h"
+
+/* TODO: replace this with something thread-safe later. */
+
+static unsigned log_level = LOG_INFO;
+
+void vl_log_setlevel(unsigned level)
+{
+ log_level = level;
+}
+
+#define MAX_LOGLEN ((size_t)4096)
+#define MAX_LOGTIMELEN ((size_t)64) /* december 999th of 10000 A.D. at 100 o'clock */
+
+static const char *const log_names[] = {
+ "TRACE",
+ "DEBUG",
+ "INFO",
+ "WARN",
+ "ERROR",
+ NULL
+};
+
+static FILE *log_stream(unsigned level)
+{
+ switch (level) {
+ case LOG_TRACE:
+ case LOG_DEBUG:
+ case LOG_INFO:
+ return stdout;
+ default:
+ return stderr;
+ }
+}
+
+void vl_logv(unsigned level, const char *fmt, va_list args)
+{
+ char buf[MAX_LOGLEN];
+ char datebuf[MAX_LOGTIMELEN];
+ time_t now;
+ struct tm now_tm;
+
+ if (log_level > level) return;
+ if (level > LOG_ERROR) return;
+
+ vsnprintf(buf, MAX_LOGLEN, fmt, args);
+ strcpy(buf + MAX_LOGLEN - 4, "...");
+
+ now = time(NULL);
+ localtime_r(&now, &now_tm);
+
+ if (!strftime(datebuf, MAX_LOGTIMELEN, "%H:%M:%S", &now_tm)) {
+ strcpy(datebuf, "???");
+ }
+
+ fprintf(log_stream(level), "[%s] %s: %s\n", datebuf, log_names[level], buf);
+}
+
+void vl_log(unsigned level, const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ vl_logv(level, fmt, args);
+ va_end(args);
+}
diff --git a/lib/macros.h b/lib/macros.h
new file mode 100644
index 0000000..5c1ba5d
--- /dev/null
+++ b/lib/macros.h
@@ -0,0 +1,6 @@
+#ifndef VL_MACROS_H_INCLUDED
+#define VL_MACROS_H_INCLUDED
+
+#define VL_USER_AGENT "vacuum launcher/0.1.0 (olauncher)"
+
+#endif
diff --git a/lib/manifest.c b/lib/manifest.c
new file mode 100644
index 0000000..c4d9bda
--- /dev/null
+++ b/lib/manifest.c
@@ -0,0 +1,4 @@
+#include <curl/curl.h>
+#include <jansson.h>
+
+
diff --git a/lib/meson.build b/lib/meson.build
new file mode 100644
index 0000000..d2fca24
--- /dev/null
+++ b/lib/meson.build
@@ -0,0 +1,5 @@
+vaclaunch_lib_inc = include_directories('include')
+vaclaunch_libs = both_libraries('vaclaunch',
+ files('manifest.c', 'log.c', 'net.c', 'sha1.c', 'arena.c'),
+ include_directories : vaclaunch_lib_inc,
+ dependencies : [ curl_dep, jansson_dep ])
diff --git a/lib/net.c b/lib/net.c
new file mode 100644
index 0000000..7509ac0
--- /dev/null
+++ b/lib/net.c
@@ -0,0 +1,274 @@
+#include "log.h"
+#include "arena.h"
+#include "macros.h"
+#include "sha1.h"
+#include "net.h"
+
+#include <libgen.h> /* for dirname/basename */
+#include <curl/curl.h>
+#include <jansson.h>
+#include <errno.h>
+#include <string.h>
+
+/* TODO: also support etag/if-not-match rather than just last-modified/if-not-modified */
+
+/* memory usage is about 3 * strlen(path) -- what da heeeellll */
+static char *derive_meta_path(vl_arena *arena, const char *path)
+{
+ char *base = basename(vl_arena_strdup(arena, path));
+ char *dname = dirname(vl_arena_strdup(arena, path));
+ return vl_arena_sprintf(arena, "%s/.%s.meta", dname, base);
+}
+
+#define NET_BUFSIZE (4096)
+
+static int hash_file(const char *fname, vl_sha1 ohash)
+{
+ FILE *file;
+ unsigned char data[NET_BUFSIZE];
+ size_t nread;
+ int ret = -1;
+ vl_sha1_st hashst;
+
+ file = fopen(fname, "rb");
+
+ if (!file) {
+ vl_debug("failed to hash file %s: %s", fname, strerror(errno));
+ return -1;
+ }
+
+ vl_sha1_init(&hashst);
+ while ((nread = fread(data, 1, NET_BUFSIZE, file)) > 0) {
+ vl_sha1_update(&hashst, data, nread);
+ }
+
+ if (ferror(file)) {
+ vl_debug("failed to read file %s :(", fname);
+ goto cleanup;
+ }
+
+ vl_sha1_finalize(&hashst, ohash);
+
+ ret = 0;
+
+cleanup:
+ fclose(file);
+ return ret;
+}
+
+static int check_cache_consistent(vl_arena *arena, const char *path, const char *meta_path, struct curl_slist **headers)
+{
+ json_error_t jerr;
+ int ret = -1;
+ json_t *j = json_load_file(meta_path, 0, &jerr);
+ char *header = NULL;
+
+ vl_trace("checking meta file %s", meta_path);
+
+ if (!j) {
+ vl_debug("failed to load json meta %s(%d:%d:%d): %s", jerr.source, jerr.line, jerr.column, jerr.position, jerr.text);
+ goto cleanup;
+ }
+
+ const char *lm;
+ const char *sha1_hex;
+ size_t sha1_len;
+
+ if (json_unpack_ex(j, &jerr, JSON_STRICT, "{s:s, s:s%}", "lm", &lm, "sha1", &sha1_hex, &sha1_len) < 0) {
+ vl_debug("failed to unpack json meta %s: %s", meta_path, jerr.text);
+ goto cleanup;
+ }
+
+ if (sha1_len != VL_SHA1_DIGEST_HEX_STRLEN) {
+ vl_debug("failed to read json meta %s: invalid sha1 digest (is %zu chars long, expected %u)", meta_path, sha1_len, VL_SHA1_DIGEST_HEX_STRLEN);
+ goto cleanup;
+ }
+
+ vl_sha1 hashgot, hashexp;
+ if (vl_sha1_decode(hashexp, sha1_hex) < 0) {
+ vl_debug("failed to read json meta %s: invalid sha1 digest (bad format)", meta_path);
+ goto cleanup;
+ }
+
+ if (hash_file(path, hashgot) < 0) {
+ goto cleanup;
+ }
+
+ if (memcmp(hashexp, hashgot, sizeof(vl_sha1)) != 0) {
+#ifdef LOG_DEBUG_ENABLED
+ char hash_exp_hex[VL_SHA1_DIGEST_HEX_STRLEN + 1];
+ char hash_got_hex[VL_SHA1_DIGEST_HEX_STRLEN + 1];
+
+ hash_exp_hex[VL_SHA1_DIGEST_HEX_STRLEN] = 0;
+ hash_got_hex[VL_SHA1_DIGEST_HEX_STRLEN] = 0;
+
+ vl_sha1_encode(hashexp, hash_exp_hex);
+ vl_sha1_encode(hashgot, hash_got_hex);
+
+ vl_debug("file %s tampered on disk (sha1 expect %s, got %s)", path, hash_exp_hex, hash_got_hex);
+#endif
+
+ goto cleanup;
+ }
+
+ header = vl_arena_sprintf(arena, "If-Modified-Since: %s", lm);
+ *headers = curl_slist_append(*headers, header);
+ ret = 0;
+
+cleanup:
+ if (j) json_decref(j);
+ return ret;
+}
+
+static void write_transfer_meta(CURL *easy, vl_sha1 ohash, const char *meta_path)
+{
+ json_t *metaj = NULL;
+ struct curl_header *hdr;
+ CURLHcode hcode;
+ char hash_hex[VL_SHA1_DIGEST_HEX_STRLEN + 1];
+
+ vl_trace("writing transfer meta to %s", meta_path);
+
+ hash_hex[VL_SHA1_DIGEST_HEX_STRLEN] = '\0';
+
+ hcode = curl_easy_header(easy, "Last-Modified", 0, CURLH_HEADER, -1, &hdr);
+ if (hcode != CURLHE_OK) {
+ vl_debug("Not writing meta %s: curl_easy_header(Last-Modified): %u", meta_path, hcode);
+ goto cleanup;
+ }
+
+ vl_sha1_encode(ohash, hash_hex);
+ metaj = json_pack("{s:s, s:s}", "lm", hdr->value, "sha1", hash_hex);
+ if (!metaj) {
+ vl_debug("Not writing meta %s: json_pack returned NULL (weird)", meta_path);
+ goto cleanup;
+ }
+
+ if (json_dump_file(metaj, meta_path, JSON_COMPACT) < 0) {
+ vl_debug("Failed writing meta to %s: json_dump_file", meta_path);
+ goto cleanup;
+ }
+
+cleanup:
+ if (metaj) json_decref(metaj);
+}
+
+struct write_ctx
+{
+ const char *opath;
+ FILE *ofile;
+ vl_sha1_st sha1_state;
+};
+
+static size_t handle_write(char *ptr, size_t sz, size_t nmemb, void *user)
+{
+ struct write_ctx *ctx = user;
+ if (sz * nmemb == 0) return 0;
+
+ /* Note that the output file is opened lazily, because we might receive a 304 (Not Modified)
+ * response with an empty body (in such a case, this function could be called with nmemb == 0).
+ * fopen for writing truncates the file, so we can't open the file in the main function.
+ */
+ if (!ctx->ofile) {
+ ctx->ofile = fopen(ctx->opath, "wb");
+ if (!ctx->ofile) {
+ vl_warn("Failed to open output file: fopen(%s, wb): %s", ctx->opath, strerror(errno));
+ return CURL_WRITEFUNC_ERROR;
+ }
+ }
+
+ vl_sha1_update(&ctx->sha1_state, ptr, sz * nmemb);
+ return fwrite(ptr, sz, nmemb, ctx->ofile);
+}
+
+int vl_net_ensure_cached(vl_arena *arena, const char *url, const char *target_path)
+{
+ char *meta_path = derive_meta_path(arena, target_path);
+ int ret = -1;
+ char errbuf[CURL_ERROR_SIZE];
+ CURLcode ecode;
+ CURL *easy = NULL;
+ struct curl_slist *headers = NULL;
+ struct write_ctx wrctx = { .opath = target_path, .ofile = NULL };
+ vl_sha1 ohash;
+ long response_code;
+
+ vl_trace("Downloading cached file from %s to %s (%s)", url, target_path, meta_path);
+
+ check_cache_consistent(arena, target_path, meta_path, &headers);
+
+ easy = curl_easy_init();
+ if (!easy) {
+ vl_warn("Failed to set up CURL handle to download %s!", url);
+ goto cleanup;
+ }
+
+ errbuf[0] = '\0';
+
+ if ((ecode = curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, errbuf)) != CURLE_OK) {
+ vl_warn("curl_easy_setopt(CURLOPT_ERRORBUFFER) failed: %s", curl_easy_strerror(ecode));
+ goto cleanup;
+ }
+
+#define CHECK(_ex) do { \
+ if ((ecode = (_ex)) != CURLE_OK) { \
+ vl_warn("%s failed: %s (%s)", #_ex, curl_easy_strerror(ecode), errbuf); \
+ goto cleanup; \
+ } \
+} while (0)
+
+ CHECK(curl_easy_setopt(easy, CURLOPT_URL, url));
+ CHECK(curl_easy_setopt(easy, CURLOPT_USERAGENT, VL_USER_AGENT));
+ CHECK(curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L));
+ CHECK(curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, CURLFOLLOW_ALL));
+ CHECK(curl_easy_setopt(easy, CURLOPT_TIMEOUT, 60));
+
+ if (headers) {
+ CHECK(curl_easy_setopt(easy, CURLOPT_HTTPHEADER, headers));
+ }
+
+ vl_sha1_init(&wrctx.sha1_state);
+
+ CHECK(curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &handle_write));
+ CHECK(curl_easy_setopt(easy, CURLOPT_WRITEDATA, &wrctx));
+
+ CHECK(curl_easy_perform(easy));
+
+ CHECK(curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code));
+
+#undef CHECK
+
+ if (response_code == 304) {
+ vl_info("Downloaded file %s not modified.", target_path);
+ ret = 0;
+ goto cleanup;
+ } else if (response_code / 100 == 2) {
+ vl_trace("download %s success: %ld", url, response_code);
+ } else {
+ vl_warn("Bad HTTP response code %ld downloading %s", response_code, url);
+ goto cleanup;
+ }
+
+ if (wrctx.ofile) {
+ fclose(wrctx.ofile);
+ wrctx.ofile = NULL;
+ }
+
+ vl_sha1_finalize(&wrctx.sha1_state, ohash);
+ write_transfer_meta(easy, ohash, meta_path);
+
+ ret = 0;
+
+cleanup:
+ if (easy) curl_easy_cleanup(easy);
+ if (headers) curl_slist_free_all(headers);
+ if (wrctx.ofile) {
+ if (ret < 0 && remove(target_path) < 0) {
+ vl_debug("... failed to clean up after failed download: unlink(%s): %s", target_path, strerror(errno));
+ }
+
+ fclose(wrctx.ofile);
+ }
+
+ return ret;
+}
diff --git a/lib/sha1.c b/lib/sha1.c
new file mode 100644
index 0000000..ed456c8
--- /dev/null
+++ b/lib/sha1.c
@@ -0,0 +1,237 @@
+#include "sha1.h"
+#include <string.h>
+
+/* for byteswap functions (TODO: avoid GNU extensions) */
+#include <endian.h>
+
+/* code adapted from https://git.figboot.dev/l2su/tree/src/digest/sha1.c */
+
+#define SHA1_H0 UINT32_C(0x67452301)
+#define SHA1_H1 UINT32_C(0xEFCDAB89)
+#define SHA1_H2 UINT32_C(0x98BADCFE)
+#define SHA1_H3 UINT32_C(0x10325476)
+#define SHA1_H4 UINT32_C(0xC3D2E1F0)
+
+#define SHA1_K0 UINT32_C(0x5A827999)
+#define SHA1_K1 UINT32_C(0x6ED9EBA1)
+#define SHA1_K2 UINT32_C(0x8F1BBCDC)
+#define SHA1_K3 UINT32_C(0xCA62C1D6)
+
+void vl_sha1_init(vl_sha1_st *st)
+{
+ st->vl_state[0] = SHA1_H0;
+ st->vl_state[1] = SHA1_H1;
+ st->vl_state[2] = SHA1_H2;
+ st->vl_state[3] = SHA1_H3;
+ st->vl_state[4] = SHA1_H4;
+
+ memset(st->vl_chunk, 0, VL_SHA1_CHUNKLEN_BYTES);
+ st->vl_nchunk = 0;
+ st->vl_total = 0;
+}
+
+static uint32_t rol(uint32_t in, uint32_t v)
+{
+ return (in << v) | (in >> (32 - v));
+}
+
+static void sha1_upd(vl_sha1_st *st)
+{
+ uint32_t w[80];
+ uint32_t a, b, c, d, e, f, k, temp;
+ unsigned i;
+
+ memcpy(w, st->vl_chunk, VL_SHA1_CHUNKLEN_BYTES);
+ for (i = 0; i < 16; ++i) {
+ w[i] = htobe32(w[i]);
+ }
+
+ for (i = 16; i < 80; ++i) {
+ w[i] = rol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
+ }
+
+ a = st->vl_state[0];
+ b = st->vl_state[1];
+ c = st->vl_state[2];
+ d = st->vl_state[3];
+ e = st->vl_state[4];
+
+#define UP \
+ temp = rol(a, 5) + f + e + k + w[i]; \
+ e = d; \
+ d = c; \
+ c = rol(b, 30); \
+ b = a; \
+ a = temp;
+
+#define OP(_start, _end, _k, _e) \
+ for (i = _start; i < _end; ++i) { \
+ f = _e; \
+ k = _k; \
+ UP \
+ }
+
+ OP( 0, 20, SHA1_K0, (b & c) ^ ((~b) & d))
+ OP(20, 40, SHA1_K1, b ^ c ^ d)
+ OP(40, 60, SHA1_K2, (b & c) ^ (b & d) ^ (c & d))
+ OP(60, 80, SHA1_K3, b ^ c ^ d)
+
+#undef OP
+#undef UP
+
+ st->vl_state[0] += a;
+ st->vl_state[1] += b;
+ st->vl_state[2] += c;
+ st->vl_state[3] += d;
+ st->vl_state[4] += e;
+}
+
+void vl_sha1_update(vl_sha1_st *st, const void *data, size_t sz)
+{
+ const uint8_t *dbytes = data;
+ size_t rem;
+ st->vl_total += sz;
+
+ while (sz >= (rem = (VL_SHA1_CHUNKLEN_BYTES - st->vl_nchunk))) {
+ memcpy(st->vl_chunk + st->vl_nchunk, dbytes, rem);
+ sha1_upd(st);
+ st->vl_nchunk = 0;
+
+ dbytes += rem;
+ sz -= rem;
+ }
+
+ if (sz > 0) {
+ memcpy(st->vl_chunk + st->vl_nchunk, dbytes, sz);
+ st->vl_nchunk += sz;
+ }
+}
+
+void vl_sha1_finalize(vl_sha1_st *st, vl_sha1 out)
+{
+ /* SHA1 specifies a 1 bit to follow the data */
+ st->vl_chunk[st->vl_nchunk] = UINT8_C(0x80);
+ ++st->vl_nchunk;
+ if (st->vl_nchunk > (VL_SHA1_CHUNKLEN_BYTES - 8)) {
+ /* must pad the rest of the way */
+ memset(st->vl_chunk + st->vl_nchunk, 0, VL_SHA1_CHUNKLEN_BYTES - st->vl_nchunk);
+ sha1_upd(st);
+ st->vl_nchunk = 0;
+ }
+
+ size_t rem = (VL_SHA1_CHUNKLEN_BYTES - st->vl_nchunk) - 8;
+ uint64_t lenbe = htobe64(st->vl_total * 8);
+ memset(st->vl_chunk + st->vl_nchunk, 0, rem);
+ st->vl_nchunk += rem;
+
+ memcpy(st->vl_chunk + st->vl_nchunk, (uint8_t *)&lenbe, 8);
+ sha1_upd(st);
+
+ uint32_t pout_i[VL_SHA1_STATELEN_32];
+ pout_i[0] = htobe32(st->vl_state[0]);
+ pout_i[1] = htobe32(st->vl_state[1]);
+ pout_i[2] = htobe32(st->vl_state[2]);
+ pout_i[3] = htobe32(st->vl_state[3]);
+ pout_i[4] = htobe32(st->vl_state[4]);
+ memcpy(out, pout_i, sizeof(vl_sha1));
+}
+
+/* shortcut for hashing a buffer */
+void vl_sha1_buffer(vl_sha1 odigest, const void *data, size_t sz)
+{
+ vl_sha1_st st;
+ vl_sha1_init(&st);
+ vl_sha1_update(&st, data, sz);
+ vl_sha1_finalize(&st, odigest);
+}
+
+static const char nibs[] = "0123456789abcdef";
+
+/* converts digest bytes to a hex string (does NOT place a NUL byte) */
+void vl_sha1_encode(const vl_sha1 dig, char hex[VL_SHA1_DIGEST_HEX_STRLEN])
+{
+ for (unsigned i = 0; i < VL_SHA1_DIGEST_BYTES; ++i) {
+ hex[i * 2] = nibs[dig[i] >> 4]; /* unsigned shift, dig[i] is 8 bytes */
+ hex[i * 2 + 1] = nibs[dig[i] & 0xf];
+ }
+}
+
+#define NIB_BAD (~0u)
+
+static unsigned decode_nib(char c)
+{
+ switch (c) {
+ case '0': return 0;
+ case '1': return 1;
+ case '2': return 2;
+ case '3': return 3;
+ case '4': return 4;
+ case '5': return 5;
+ case '6': return 6;
+ case '7': return 7;
+ case '8': return 8;
+ case '9': return 9;
+ case 'a': case 'A': return 10;
+ case 'b': case 'B': return 11;
+ case 'c': case 'C': return 12;
+ case 'd': case 'D': return 13;
+ case 'e': case 'E': return 14;
+ case 'f': case 'F': return 15;
+ default: return NIB_BAD;
+ }
+}
+
+/* converts a hex string to digest bytes */
+int vl_sha1_decode(vl_sha1 odigest, const char hex[VL_SHA1_DIGEST_HEX_STRLEN])
+{
+ for (unsigned i = 0; i < VL_SHA1_DIGEST_BYTES; ++i) {
+ unsigned nibhi = decode_nib(hex[i * 2]);
+ unsigned niblo = decode_nib(hex[i * 2 + 1]);
+ if ((nibhi | niblo) == NIB_BAD) return -1;
+ odigest[i] = (uint8_t)((nibhi << 4) | niblo);
+ }
+
+ return 0;
+}
+
+#ifdef SHA1_STANDALONE_TEST
+
+#include <unistd.h>
+#include <errno.h>
+#include <stdio.h>
+
+int main(void)
+{
+ unsigned char chunk[4096];
+ char dig[VL_SHA1_DIGEST_HEX_STRLEN + 1];
+ vl_sha1_st st;
+
+ dig[VL_SHA1_DIGEST_HEX_STRLEN] = '\0';
+ vl_sha1_init(&st);
+
+ while (1) {
+ ssize_t nread = read(0, chunk, 4096);
+ if (nread < 0) {
+ fprintf(stderr, "read: %s\n", strerror(errno));
+ return 1;
+ } else if (nread == 0) {
+ break;
+ }
+
+ vl_sha1_update(&st, chunk, nread);
+ }
+
+ vl_sha1 o;
+ vl_sha1_finalize(&st, o);
+
+ vl_sha1_encode(o, dig);
+ printf("%s\n", dig);
+
+ vl_sha1 o2;
+ vl_sha1_decode(o2, dig);
+ printf("cmp: %d\n", memcmp(o, o2, sizeof(vl_sha1)));
+
+ return 0;
+}
+
+#endif