summaryrefslogtreecommitdiff
path: root/surveil
diff options
context:
space:
mode:
authorJohn Vogel <jvogel4@stny.rr.com>2024-07-21 16:30:48 -0400
committerJohn Vogel <jvogel4@stny.rr.com>2024-07-21 16:30:48 -0400
commitd161261b35b5ab2ba561a21dbd9420f3d2496895 (patch)
tree9dd1cbe7053723532873c1dff9ff217dd6baca24 /surveil
parenta7dfcf130cdeeba7fee0a1ff6bf44bd746767bdf (diff)
downloadmy-aports-d161261b35b5ab2ba561a21dbd9420f3d2496895.tar.gz
local/surveil: new local aport
Diffstat (limited to 'surveil')
-rw-r--r--surveil/APKBUILD77
-rw-r--r--surveil/surveil/LICENSE22
-rw-r--r--surveil/surveil/Makefile55
-rw-r--r--surveil/surveil/NOTES60
-rw-r--r--surveil/surveil/README.md39
-rw-r--r--surveil/surveil/buf.c63
-rw-r--r--surveil/surveil/buf.h22
-rw-r--r--surveil/surveil/common.h13
-rw-r--r--surveil/surveil/config.c300
-rw-r--r--surveil/surveil/config.h28
-rw-r--r--surveil/surveil/db.c368
-rw-r--r--surveil/surveil/db.h19
-rw-r--r--surveil/surveil/dgst.c118
-rw-r--r--surveil/surveil/dgst.h13
-rw-r--r--surveil/surveil/diff.c55
-rw-r--r--surveil/surveil/diff.h8
-rw-r--r--surveil/surveil/example.conf22
-rw-r--r--surveil/surveil/job.c97
-rw-r--r--surveil/surveil/job.h25
-rw-r--r--surveil/surveil/main.c489
-rw-r--r--surveil/surveil/net.c107
-rw-r--r--surveil/surveil/net.h11
-rw-r--r--surveil/surveil/search.c241
-rw-r--r--surveil/surveil/search.h26
-rw-r--r--surveil/surveil/surveil.182
-rw-r--r--surveil/surveil/util.c219
-rw-r--r--surveil/surveil/util.h18
27 files changed, 2597 insertions, 0 deletions
diff --git a/surveil/APKBUILD b/surveil/APKBUILD
new file mode 100644
index 0000000..46a1804
--- /dev/null
+++ b/surveil/APKBUILD
@@ -0,0 +1,77 @@
+# Contributor: John Vogel <jvogel4@stny.rr.com>
+# Maintainer: John Vogel <jvogel4@stny.rr.com>
+pkgname=surveil
+pkgver=0.1
+pkgrel=2
+pkgdesc="trach changes to remote files"
+url="http://localhost/home/john/Code/surveil"
+arch="all"
+license="MIT"
+makedepends="curl-dev openssl-dev inih-dev xdiff-dev sqlite-dev libqueue-dev"
+subpackages="$pkgname-doc"
+source="surveil/LICENSE
+ surveil/Makefile
+ surveil/NOTES
+ surveil/README.md
+ surveil/buf.c
+ surveil/buf.h
+ surveil/common.h
+ surveil/config.c
+ surveil/config.h
+ surveil/db.c
+ surveil/db.h
+ surveil/dgst.c
+ surveil/dgst.h
+ surveil/diff.c
+ surveil/diff.h
+ surveil/example.conf
+ surveil/job.c
+ surveil/job.h
+ surveil/main.c
+ surveil/net.c
+ surveil/net.h
+ surveil/search.c
+ surveil/search.h
+ surveil/surveil.1
+ surveil/util.c
+ surveil/util.h"
+options="!check"
+
+builddir="$srcdir/"
+
+build() {
+ make
+}
+
+package() {
+ make DESTDIR="$pkgdir" PREFIX=/usr install
+}
+
+sha512sums="
+9aa08e37eca237a54b10ac6a78a8e496641b7fdfa6a220e9d59f1db061cdf13165619ea4cda129e8d97afd9dfa4630f9d923df1c70a7f7a7e65dd7420b1329f0 LICENSE
+d2ea7eb831f22576fe7e81c6dea3e7d2c9e9fad1dae63f15926142aaa56ba26f6c55c8cf58edb3cd5be1641ae12fb38fdf0fdfc52ff419e16b0dac74fe8bb064 Makefile
+a061db311e7ba7fcb29659eb584b0b530bdd69324966036f28046c2931ee63f23a07ed0e8c06b0aa6ff8619134e11f8ed4ba31a6d5717a6d67a53498651166bc NOTES
+f6767772a8d024c156d2f5b6baf635c159ea2939ecdc25e1d4d55189d22f0bd00da6a0ab6ad7f02f38d01bbb5c564d7db743fcb81a8b4a1dd413cf21986b91f3 README.md
+527f4f820488574bae8ab81ce3df11a95f3dc419ab6eef6d565d0a817ea2257f98c6b3152ded22521059b9999cf71f892dddc5d8f05180bd087aaa55168b3417 buf.c
+999a896fa4c896414c21cd91bb9f954e45c71aed025e8d44df0c40914b839320b88dc6da9437818509c9145f0c5dc2034c9180c0cbb370f2ab5e17ece5d90b14 buf.h
+e96e532a270e16962e3dd01df2ebb8347e06168e977f8f8a97bd480e31f7468daca8308ea7a675a44b4c3199bf658601272c5790fa97a0a7fa73995631dd4a97 common.h
+1c1906e1e11834ad4c6ad1bbd3fd4778352915c7f361678f72cd805924b238c9f75862aa2d9d86f0fb2fad959158aa87a0da83ef40b2d43f15a2942abedabcab config.c
+ac91e67d546dc8a2b66aae1a216bc271813c67a33a50eb3978e00834ef9ffd50afb16d88edbd61c1598a9484438f54f4a2bdbc73e444c5bd441f4c889d29172c config.h
+c5a3819658879ea7a7d146c79615fc8f9925a8a90efe6946434b75a215fae84e7349fab751aa22521c03548909a810b23cd2cbfc12455c634eb3a5235d9175dc db.c
+e2549a6caa1ec65f1753f44dfdfe4dd0c7baa5987be2ea13c1235b3f2388a79d5e89fda450a074120d5cd8d9cde596a89bb308c3f927ba08aab63cf357b7b140 db.h
+e8b344ea99ac5c9e47354f11e71ab0e0a65fc7df7cca9d4bf103f7801a0fe2e485833e53d2b6e1d1f4b799ab7c3b993fac3de0688ac4c389ac1ea2f584216b4b dgst.c
+bae7269e443ffa471d23079ad7ba206256ab71053f678ee89353e8bfa05ffbae1d7b964aa39eb5ba5eeb6e0fdf0d53fb9010b4315ade7491ee319adb1026dc34 dgst.h
+5c849781dcd51b78196406bcb7fa4067082ac1e9f7c62d0f812796dbc1aaee2837ab1f2c937673d72df7c9a121bc4741de12ec3351e74ffab93b13f09edc4834 diff.c
+4b745cb067b7a3e9215427a2eef4fcf617ec9f9939650921f16f33dcaa2b6ac7cf921cbc67922737d34e4c1bf7c21366fc06152fc10558618eb74d1278d867ce diff.h
+5216eb767b1cd2b65923905f28a93637049e712055badcfc8cb999908e8d748d70284c9ebe67aa52b040d557544f44a83b52c108b926ae7945fc12a35621194d example.conf
+39938dfd96c72993d8bddbc9a6d685ee18af1df61dac6039448630aa05283b63b14352594972c2ea8e96553a135234b01fe1334702ecada306bce25a717b27d6 job.c
+4ea25eb9fb63df997cc3be98cd7ef0b0f2b9750504731566f39210ba7062877cd53842aea78f7c4fb418eef841f19081b50d287495d4012d80f1fe343c04fd83 job.h
+3d3c7fd628b5a31de5f7bf123e3826c5e842d455e1f2cb4333540a2a9737b729d8b6210bd9cd70d31504461ecc41218d2fb9d376854961fb648e55fb24ce9fae main.c
+0a61764ac07548b3099ad93dfb161c0482a87a28df75ed0f8f89ef839414bffb775a17d6f8b215cb03bdefa1f5de6d1f963fdd61ad677f40388324030aca88bb net.c
+d5b2accdbf041d92f7b1874423d205a0cb9bdfcbf6a8dbbff99bd00d542ce318f5566e3634728b5b4ecedb0534be043f6a121adf4dd0c44fc5bac7cc6a34552e net.h
+8378702b8fc77a40091c6677ee001f6a8453e6a311f37422aa8342002adce4251942d60274b3cb1da048b3ded3bfa9266f3cfb6abf503b0a72bf72672a7ff4a0 search.c
+795b356d875804b39576b99c155f4fa2a954ec7c8281029e6f2f43a88b3ab6535c50bee85d0105fa30cd08329286addc11a8207ee7d6450331d2ba952b5b949d search.h
+c642b266fd3300f832cc597b02875c28ac9fb37fbc3975206b1ba665dbb64a3f8e15663d6299553b4915193c6ade418e346503f7499c7371be994c9cbd6a943e surveil.1
+a7227dc9a91339bbf7aeed27f335f9d4cc6b277c07f7d29fe81c1942513b5d9bcb22f7c2ba6050d2504728ca8ad23392bdc9957a4f3412a0478b79b89966ded2 util.c
+29006ed3c8c709ba66aa1a3b2fda403f711e58e8709d2b0d73820367f6bd9d80d23ed4250d75813241c35fd5bf7dbecb89f07a79f4fc68c131d290db7de5f994 util.h
+"
diff --git a/surveil/surveil/LICENSE b/surveil/surveil/LICENSE
new file mode 100644
index 0000000..2e52187
--- /dev/null
+++ b/surveil/surveil/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2023, John Vogel
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/surveil/surveil/Makefile b/surveil/surveil/Makefile
new file mode 100644
index 0000000..8212189
--- /dev/null
+++ b/surveil/surveil/Makefile
@@ -0,0 +1,55 @@
+# SPDX-License-Identifier: BSD-2-Clause
+# Copyright (C) 2023 John Vogel
+
+CC ?= gcc
+CFLAGS += -fPIE -std=c11 -fanalyzer -Wpedantic -Wall -Wextra -fstack-protector -D_POSIX_SOURCE
+LDFLAGS += -pie -Wl,-z,noexecstack
+LIBS = -linih -lcurl -lcrypto -lsqlite3 -lxdiff -lqueue
+INSTALL = install
+INSTALL_BIN = $(INSTALL) -m 755
+INSTALL_DATA = $(INSTALL) -m 644
+PREFIX = /usr/local
+BINDIR = $(PREFIX)/bin
+MANDIR = $(PREFIX)/share/man
+DOCDIR = $(PREFIX)/share/doc/surveil
+
+BIN = surveil
+SRCS = buf.c config.c db.c dgst.c diff.c job.c main.c net.c search.c util.c
+HDRS = buf.h common.h config.h db.h dgst.h diff.h job.h net.h search.h util.h
+OBJS = $(SRCS:.c=.o)
+
+ifdef DEBUG
+ CFLAGS += -O0 -g -ggdb -gdwarf-4
+else
+ CFLAGS += -O2
+endif
+
+.PHONY: all clean
+
+.c.o:
+ $(CC) -c $(CFLAGS) $< -o $@
+
+all: $(BIN)
+
+$(BIN): $(OBJS)
+ $(CC) -o $(BIN) $(OBJS) $(LDFLAGS) $(LIBS)
+
+$(OBJS): $(HDRS)
+
+clean:
+ $(RM) $(BIN) $(OBJS)
+
+install: $(BIN)
+ mkdir -p -m 755 $(DESTDIR)$(BINDIR)
+ mkdir -p -m 755 $(DESTDIR)$(MANDIR)
+ mkdir -p -m 755 $(DESTDIR)$(DOCDIR)
+ $(INSTALL_BIN) $(BIN) $(DESTDIR)$(BINDIR)
+ $(INSTALL_DATA) -D -t $(DESTDIR)$(MANDIR)/man1 surveil.1
+ $(INSTALL_DATA) -D -t $(DESTDIR)$(DOCDIR) README.md NOTES LICENSE example.conf
+
+uninstall:
+ rm -f $(DESTDIR)$(BINDIR)/$(BIN)
+ rm -f $(DESTDIR)$(MANDIR)/man1/surveil.1
+ rm -f $(DESTDIR)$(DOCDIR)/README.md
+ rm -f $(DESTDIR)$(DOCDIR)/NOTES
+ rm -f $(DESTDIR)$(DOCDIR)/LICENSE
diff --git a/surveil/surveil/NOTES b/surveil/surveil/NOTES
new file mode 100644
index 0000000..b4cf521
--- /dev/null
+++ b/surveil/surveil/NOTES
@@ -0,0 +1,60 @@
+Handle unused variables more gracefully.
+
+Program options:
+-h
+ prints help/usage message and exit
+
+-p
+ prints digest providers available
+
+-q
+ turn on quiet mode
+
+-t
+ test configuration and exit
+
+-v
+ turn on verbose mode
+
+-c file
+ sets a specific configuration file
+ defaults to stdout for data
+ overrides configuration directory, ignored and mutually exclusive
+ if -d, data to that file
+ if -D, data to that directory at $conf.dat
+
+-d file
+ sets a specific data file
+ defaults to configuration from $config/surveil.conf
+ or if -C dir, then $dir/surveil.dat
+ of if -c file, then $file.dat
+ override data directory, ignored and mutually exclusive
+
+-C dir
+ sets configuration directory
+ defaults to $XDG_CONFIG_HOME/$progname or $HOME/.$progname
+ main configuration defaults to $dir/$progname.conf
+
+-D dir
+ set data directory
+ defaults to $XDG_DATA_HOME/$progname or $HOME/.$progname
+ main data file defaults to $dir/$progname.dat
+
+With no command line options to change the configuration/data directories/files,
+then defaults are (searched in order listed:
+
+configuration:
+ $XDG_CONFIG_HOME/$progname
+ $HOME/.$progname
+
+data:
+ $XDG_DATA_HOME/$progname
+ $HOME/.$progname
+
+-P prunetype
+ set pruning and method, for clearing out database entries
+
+ without prune set in configuration file, defaults to none (keep all)
+ STALE: only prune db entries that don't exist in config file
+ PURGE: purge all db entries from db (empty)
+ SPECIFIC: prune db entry for command line specified jobs
diff --git a/surveil/surveil/README.md b/surveil/surveil/README.md
new file mode 100644
index 0000000..71676de
--- /dev/null
+++ b/surveil/surveil/README.md
@@ -0,0 +1,39 @@
+Surveil is used to monitor changes to web pages.
+It can track whole content from a web page or just portions of it.
+It was initially inspired by the Crux Linux utility, ck4up.
+
+Surveil depends on:
+
+ * [libcurl](https://curl.se/libcurl/)
+ * [inih](https://github.com/benhoyt/inih)
+ * [libcrypto](https://www.openssl.org/)
+ * [libqueue](https://git.stygian.me/libqueue/)
+ * [libxdiff](https://git.stygian.me/libxdiff/)
+
+Configuration is done using INI syntax, ex:
+
+ prune=off
+ quiet=off
+ verbose=off
+ [job_name]
+ url=https://www.example.com/
+ pat=progname-([[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+)\.tar\.gz
+ rpl=\1
+ dig=sha1
+
+Global options:
+
+prune - whether to clean out stale db entries
+quiet - silent mode
+verbose - output more
+
+The portion in square brackets is the job name, commonly called the section in INI sytax.
+
+url is the web page to monitor.
+
+pat is regular expression (regex(7)) pattern to search to page with (optional).
+
+rpl is the replace string (regex(7)) to use (sed style s/pat/rpl/, option).
+
+dig is the digest type to use for stored data.
+
diff --git a/surveil/surveil/buf.c b/surveil/surveil/buf.c
new file mode 100644
index 0000000..cc04e70
--- /dev/null
+++ b/surveil/surveil/buf.c
@@ -0,0 +1,63 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <stdio.h>
+#include <string.h>
+
+#include "buf.h"
+#include "util.h"
+
+int buf_init(struct buf *buf, size_t chunksize)
+{
+ size_t sz;
+
+ sz = chunksize ? ((chunksize / CHNKSZ) + 1) * CHNKSZ : CHNKSZ;
+
+ buf->ptr = xmalloc(sz);
+ buf->len = 0;
+ buf->size = buf->chunksize = sz;
+
+ return 0;
+}
+
+int buf_space(struct buf *buf, size_t more)
+{
+ char *s;
+ size_t add;
+
+ if (!buf->ptr && buf_init(buf, more) == -1)
+ return -1;
+
+ if (buf->size - buf->len <= more) {
+ add = ((more / buf->chunksize) + 1) * buf->chunksize;
+ s = xrealloc(buf->ptr, buf->size + add);
+ buf->ptr = s;
+ buf->size += add;
+ }
+
+ return 0;
+}
+
+int buf_addchr(struct buf *buf, int c)
+{
+ if (buf_space(buf, 1) == -1)
+ return -1;
+ else
+ buf->ptr[buf->len++] = c;
+
+ if (c == 0) {
+ buf->len--;
+ buf->ptr[buf->size - 1] = c;
+ }
+ return 0;
+}
+
+int buf_addstr(struct buf *buf, const char *str, size_t len)
+{
+ if (buf_space(buf, len) == 0)
+ memcpy(buf->ptr+buf->len, str, len);
+ else
+ return -1;
+
+ buf->len += len;
+
+ return 0;
+}
diff --git a/surveil/surveil/buf.h b/surveil/surveil/buf.h
new file mode 100644
index 0000000..42eed19
--- /dev/null
+++ b/surveil/surveil/buf.h
@@ -0,0 +1,22 @@
+/* vim: set ts=4 sw=4 sts=4: */
+
+#ifndef SURVEIL_BUF_H
+#define SURVEIL_BUF_H
+
+#ifndef CHNKSZ
+#define CHNKSZ 512
+#endif
+
+struct buf {
+ char *ptr;
+ size_t len;
+ size_t size;
+ size_t chunksize;
+};
+
+int buf_init(struct buf *, size_t);
+int buf_space(struct buf *, size_t);
+int buf_addchr(struct buf *, int);
+int buf_addstr(struct buf *, const char *, size_t);
+
+#endif/*!SURVEIL_BUF_H*/
diff --git a/surveil/surveil/common.h b/surveil/surveil/common.h
new file mode 100644
index 0000000..0730c26
--- /dev/null
+++ b/surveil/surveil/common.h
@@ -0,0 +1,13 @@
+/* vim: set ts=4 sw=4 ai: */
+#ifndef SURVEIL_COMMON_H
+#define SURVEIL_COMMON_H
+
+#include <queue/tq.h>
+#include "config.h"
+
+struct context {
+ struct tqh *jobs;
+ struct config *cfg;
+};
+
+#endif/*!SURVEIL_COMMON_H*/
diff --git a/surveil/surveil/config.c b/surveil/surveil/config.c
new file mode 100644
index 0000000..d42e69a
--- /dev/null
+++ b/surveil/surveil/config.c
@@ -0,0 +1,300 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <limits.h>
+#include <pwd.h>
+#include <errno.h>
+#include <err.h>
+
+#include "util.h"
+#include "config.h"
+
+static char *getuserhome(void)
+{
+ char *env;
+ struct passwd *pw;
+ uid_t euid;
+
+ if ((env = getenv("HOME")) != NULL)
+ return env;
+
+ euid = geteuid();
+ errno = 0;
+ if ((pw = getpwuid(euid)) != NULL)
+ return pw->pw_dir;
+
+ warn("getpwuid(%u)", euid);
+
+ return NULL;
+}
+
+static int config_path(char *dst, size_t n, char *env, char *home, char *dfl, char *fb, char *sub)
+{
+ char tbuf[PATH_MAX];
+ int l, r;
+ struct stat st;
+
+ /* fortify complains tbuf may be used uninitialized without this */
+ memset(tbuf, 0, sizeof(tbuf));
+
+ l = snprintf(tbuf, sizeof(tbuf), "%s%s%s",
+ env ? "" : home,
+ env ? "" : "/",
+ env ? env : dfl);
+ if ((unsigned long)l >= sizeof(tbuf)) {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+
+ r = stat(tbuf, &st) == 0 && S_ISDIR(st.st_mode);
+
+ l = snprintf(dst, n, "%s/%s", r ? tbuf : home, r ? sub : fb);
+ if ((unsigned long)l >= n ) {
+ errno = ENAMETOOLONG;
+ return -1;
+ }
+
+ if (stat(dst, &st) == -1)
+ errno = ENOENT;
+ else if (!S_ISDIR(st.st_mode))
+ errno = ENOTDIR;
+ else
+ return 0;
+
+ return -1;
+}
+
+static int get_value(const char *value, unsigned int *ret)
+{
+ unsigned int val;
+
+ if (!ret)
+ return -1;
+
+ if (s2u(value, &val) == 0)
+ *ret = val ? 1 : 0;
+ else if (strcasecmp(value, "on") == 0 ||
+ strcasecmp(value, "yes") == 0)
+ *ret = 1;
+ else if (strcasecmp(value, "off") == 0 ||
+ strcasecmp(value, "no") == 0)
+ *ret = 0;
+ else
+ return -1;
+
+ return 0;
+}
+
+int config_global(struct config *cfg, const char *name, const char *value)
+{
+ unsigned int val;
+
+ if (strcasecmp(name, "prune") == 0) {
+ if (get_value(value, &val) != 0) {
+ warnx("unrecognized value for prune setting: '%s'", value);
+ return -1;
+ }
+ cfg->prune = val;
+ }
+ else if (strcasecmp(name, "quiet") == 0) {
+ if (get_value(value, &val) != 0) {
+ warnx("unrecoggized value for quiet setting: '%s'", value);
+ return -1;
+ }
+ cfg->quiet = val;
+ }
+ else if (strcasecmp(name, "verbose") == 0) {
+ if (get_value(value, &val) != 0) {
+ warnx("unrecoggized value for verbose setting: '%s'", value);
+ return -1;
+ }
+ cfg->verbose = val;
+ }
+ else {
+ warnx("unrecognized config setting: '%s'", name);
+ return -1;
+ }
+
+
+ return 0;
+}
+
+struct config *config(char *cfgd, char *cfgf, char *datd, char *datf)
+{
+ int l;
+ size_t len;
+ char dir[PATH_MAX], file[PATH_MAX];
+ char *home, *configfile, *datafile;
+ struct stat st;
+ struct config *cfg;
+
+ cfg = NULL;
+ configfile = datafile = NULL;
+
+ home = getuserhome();
+ if (!home) {
+ warnx("unable to determine home directory");
+ return NULL;
+ }
+
+ if (cfgf) {
+ len = strlen(cfgf);
+ if (len >= PATH_MAX) {
+ errno = ENAMETOOLONG;
+ warn("%s", cfgf);
+ return NULL;
+ }
+
+ if (stat(cfgf, &st) != 0) {
+ warn("config: %s", cfgf);
+ return NULL;
+ }
+
+ configfile = xstrdup(cfgf);
+ }
+ else {
+ if (cfgd) {
+ len = strlen(cfgd);
+ if (len >= sizeof(dir)) {
+ errno = ENAMETOOLONG;
+ warn("%s", cfgd);
+ return NULL;
+ }
+
+ if (stat(cfgd, &st) != 0) {
+ warn("config: %s", cfgd);
+ return NULL;
+ }
+ else if (!S_ISDIR(st.st_mode)) {
+ errno = ENOTDIR;
+ warn("config: %s", cfgd);
+ return NULL;
+ }
+
+ memcpy(dir, cfgd, len+1);
+ }
+ else if (config_path(dir, sizeof(dir), getenv("XDG_CONFIG_HOME"),
+ home, CONFIG_HOME_DEFAULT, CONFIG_HOME_FALLBACK, "surveil") == -1) {
+ if (errno == ENAMETOOLONG)
+ warn("config directory path");
+ else
+ warn("config directory path (%s)", dir);
+
+ return NULL;
+ }
+
+ l = snprintf(file, sizeof(file), "%s/%s", dir, CONFIG_FILE_DEFAULT);
+ if ((unsigned long)l > sizeof(file)) {
+ errno = ENAMETOOLONG;
+ warn("config file path");
+ return NULL;
+ }
+
+ if (stat(file, &st) != 0) {
+ warn("config: %s", file);
+ return NULL;
+ }
+
+ configfile = xstrdup(file);
+ }
+
+ if (datf) {
+ len = strlen(datf);
+ if (len >= PATH_MAX) {
+ errno = ENAMETOOLONG;
+ warn("%s", datf);
+ goto config_fail;
+ }
+
+ datafile = xstrdup(datf);
+ }
+ else {
+ char name[PATH_MAX];
+ char *tname, *dot;
+
+ if (datd) {
+ len = strlen(datd);
+ if (len >= sizeof(dir)) {
+ errno = ENAMETOOLONG;
+ warn("%s", datd);
+ goto config_fail;
+ }
+ memcpy(dir, datd, len+1);
+ }
+ else if (config_path(dir, sizeof(dir), getenv("XDG_DATA_HOME"),
+ home, DATA_HOME_DEFAULT, DATA_HOME_FALLBACK, "surveil") == -1) {
+ if (errno == ENAMETOOLONG) {
+ warn("data directory path");
+ goto config_fail;
+ }
+ warn("%s", dir);
+ }
+
+ tname = cfgf ? cfgf : DATA_FILE_DEFAULT;
+ memcpy(name, tname, strlen(tname)+1);
+ tname = strrchr(name, '/');
+ if (tname)
+ tname++;
+ else
+ tname = name;
+ dot = strrchr(tname, '.');
+ if (dot && dot != tname)
+ *dot = '\0';
+
+ l = snprintf(file, sizeof(file), "%s/%s.db", dir, tname);
+ if ((unsigned long)l > sizeof(file)) {
+ errno = ENAMETOOLONG;
+ warn("%s", file);
+ goto config_fail;
+ }
+
+ if (stat(dir, &st) == -1)
+ warn("config: %s", dir);
+ else if (!S_ISDIR(st.st_mode)) {
+ errno = ENOTDIR;
+ warn("config: %s", dir);
+ goto config_fail;
+ }
+
+ datafile = xstrdup(file);
+ }
+
+ cfg = xmalloc(sizeof(struct config));
+ cfg->prune = 0;
+ cfg->quiet = 0;
+ cfg->verbose = 0;
+ cfg->configfile = configfile;
+ cfg->datafile = datafile;
+
+config_fail:
+ if (!cfg) {
+ free(configfile);
+ free(datafile);
+ }
+
+ return cfg;
+}
+
+void config_cleanup(struct config *cfg)
+{
+ if (cfg->nspecified)
+ free(cfg->specified);
+ free(cfg->configfile);
+ free(cfg->datafile);
+ free(cfg);
+}
+
+int name_is_specified(struct config *cfg, const char *name)
+{
+ size_t i;
+
+ for (i = 0; i < cfg->nspecified; i++)
+ if (strcmp(cfg->specified[i], name) == 0)
+ return 1;
+
+ return 0;
+}
diff --git a/surveil/surveil/config.h b/surveil/surveil/config.h
new file mode 100644
index 0000000..3a1935d
--- /dev/null
+++ b/surveil/surveil/config.h
@@ -0,0 +1,28 @@
+/* vim: set ts=4 sw=4: */
+#ifndef SURVEIL_CONFIG_H
+#define SURVEIL_CONFIG_H
+
+#define CONFIG_HOME_DEFAULT ".config"
+#define DATA_HOME_DEFAULT ".local/share"
+#define CONFIG_HOME_FALLBACK ".surveil"
+#define DATA_HOME_FALLBACK ".surveil"
+#define CONFIG_FILE_DEFAULT "surveil.conf"
+#define DATA_FILE_DEFAULT "surveil.db"
+
+struct config {
+ unsigned int prune;
+ unsigned int quiet;
+ unsigned int verbose;
+ unsigned int test;
+ char *configfile;
+ char *datafile;
+ char **specified;
+ size_t nspecified;
+};
+
+int config_global(struct config *, const char *, const char *);
+struct config *config(char *, char *, char *, char *);
+int name_is_specified(struct config *, const char *);
+void config_cleanup(struct config *);
+
+#endif/*!SURVEIL_CONFIG_H*/
diff --git a/surveil/surveil/db.c b/surveil/surveil/db.c
new file mode 100644
index 0000000..2a337f1
--- /dev/null
+++ b/surveil/surveil/db.c
@@ -0,0 +1,368 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <limits.h>
+#include <time.h>
+#include <err.h>
+#include <sqlite3.h>
+#include <queue/tq.h>
+
+#include "db.h"
+#include "job.h"
+#include "util.h"
+
+#define TABLE_NAME "data"
+
+#define CREATE_TABLE "CREATE TABLE IF NOT EXISTS " TABLE_NAME \
+ " (name TEXT PRIMARY KEY, url TEXT, date INTEGER, buf BLOB);"
+
+#define INSERT "INSERT INTO " TABLE_NAME " (name, url, date, buf)" \
+ " VALUES (?,?,?,?)"\
+ " ON CONFLICT(name) DO UPDATE" \
+ " SET url = excluded.url, date = excluded.date, buf = excluded.buf;"
+
+#define UPDATE "UPDATE " TABLE_NAME " SET url = ?, date = ?, buf = ?" \
+ " WHERE name = ?;"
+
+#define DELETE "DELETE FROM " TABLE_NAME " WHERE name = ?;"
+
+#define FIND "SELECT url,date,buf FROM " TABLE_NAME " WHERE name = ?;"
+
+#define FIND_ALL "SELECT name,url,date,buf FROM " TABLE_NAME ";"
+
+static sqlite3 *db;
+
+int db_init(const char *path)
+{
+ int res, ret = -1;
+ sqlite3_stmt *stmt = NULL;
+ const char *leftover = NULL;
+ const char *cmd = CREATE_TABLE;
+
+ if (sqlite3_open(path, &db) != SQLITE_OK) {
+ warnx("%s: failed to open sqlite database %s: %s",
+ __func__, path, sqlite3_errmsg(db));
+ goto db_init_fail;
+ }
+
+ res = sqlite3_prepare_v2(db, cmd, -1, &stmt, &leftover);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_prepare_v2: %s", __func__, sqlite3_errmsg(db));
+ goto db_init_fail;
+ }
+ if (leftover && leftover < cmd+strlen(cmd)) {
+ warnx("%s: unused text in SQL statement: %s in %s",
+ __func__, leftover, cmd);
+ goto db_init_fail;
+ }
+
+ res = sqlite3_step(stmt);
+ if (res != SQLITE_DONE) {
+ warnx("%s: sqlite3_step returned %d, expected SQLITE_DONE(%d)",
+ __func__, res, SQLITE_DONE);
+ goto db_init_fail;
+ }
+
+ ret = 0;
+
+db_init_fail:
+ sqlite3_finalize(stmt);
+
+ if (ret == -1)
+ sqlite3_close(db);
+
+ return ret;
+}
+
+int db_insert(const struct job *j)
+{
+ int res, ret = -1;
+ sqlite3_stmt *stmt = NULL;
+ const char *leftover = NULL;
+ const char *cmd = INSERT;
+
+ res = sqlite3_prepare_v2(db, cmd, -1, &stmt, &leftover);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_prepare_v2: %s", __func__, sqlite3_errmsg(db));
+ return -1;
+ }
+ if (leftover && leftover < cmd+strlen(cmd)) {
+ warnx("%s: unused text in SQL statement: %s in %s",
+ __func__, leftover, cmd);
+ goto db_insert_fail;
+ }
+ res = sqlite3_bind_text(stmt, 1, j->name, strlen(j->name), SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(name,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_insert_fail;
+ }
+ res = sqlite3_bind_text(stmt, 2, j->url, strlen(j->url), SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(url,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_insert_fail;
+ }
+ res = sqlite3_bind_int64(stmt, 3, j->date);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_int64(date,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_insert_fail;
+ }
+ res = sqlite3_bind_blob(stmt, 4, j->buf, j->bufsz, SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(dtype,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_insert_fail;
+ }
+
+ res = sqlite3_step(stmt);
+ if (res != SQLITE_DONE) {
+ warnx("sqlite3_step returned %d, expected SQLITE_DONE(%d)",
+ res, SQLITE_DONE);
+ goto db_insert_fail;
+ }
+
+ ret = 0;
+
+db_insert_fail:
+ sqlite3_finalize(stmt);
+
+ return ret;
+}
+
+int db_update(const struct job *j)
+{
+ int res, ret = -1;
+ sqlite3_stmt *stmt = NULL;
+ const char *leftover = NULL;
+ const char *cmd = UPDATE;
+
+ res = sqlite3_prepare_v2(db, cmd, -1, &stmt, &leftover);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_prepare_v2: %s", __func__, sqlite3_errmsg(db));
+ return -1;
+ }
+ if (leftover && leftover < cmd+strlen(cmd)) {
+ warnx("%s: unused text in SQL statement: %s in %s",
+ __func__, leftover, cmd);
+ goto db_update_fail;
+ }
+ res = sqlite3_bind_text(stmt, 1, j->url, strlen(j->url), SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(url,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_update_fail;
+ }
+ res = sqlite3_bind_int64(stmt, 2, j->date);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_int64(date,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_update_fail;
+ }
+ res = sqlite3_bind_blob(stmt, 3, j->buf, j->bufsz, SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(buf,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_update_fail;
+ }
+ res = sqlite3_bind_text(stmt, 4, j->name, strlen(j->name), SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(name,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_update_fail;
+ }
+
+ res = sqlite3_step(stmt);
+ if (res != SQLITE_DONE) {
+ warnx("%s: sqlite3_step returned %d, expected SQLITE_DONE(%d)",
+ __func__, res, SQLITE_DONE);
+ goto db_update_fail;
+ }
+
+ ret = 0;
+
+db_update_fail:
+ sqlite3_finalize(stmt);
+
+ return ret;
+}
+
+int db_delete(const char *name)
+{
+ int res, ret = -1;
+ sqlite3_stmt *stmt = NULL;
+ const char *leftover = NULL;
+ const char *cmd = DELETE;
+
+ res = sqlite3_prepare_v2(db, cmd, -1, &stmt, &leftover);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_prepare_v2: %s", __func__, sqlite3_errmsg(db));
+ return -1;
+ }
+ if (leftover && leftover < cmd+strlen(cmd)) {
+ warnx("%s: unused text in SQL statement: %s in %s", __func__, leftover, cmd);
+ goto db_delete_fail;
+ }
+ res = sqlite3_bind_text(stmt, 1, name, strlen(name), SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(name,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_delete_fail;
+ }
+
+ res = sqlite3_step(stmt);
+ if (res != SQLITE_DONE) {
+ warnx("%s: sqlite3_step returned %d, expected SQLITE_DONE(%d)",
+ __func__, res, SQLITE_DONE);
+ goto db_delete_fail;
+ }
+
+ ret = 0;
+
+db_delete_fail:
+ sqlite3_finalize(stmt);
+
+ return ret;
+}
+
+int db_find(const char *name, struct job **retval)
+{
+ int res, ret = -1;
+ sqlite3_stmt *stmt = NULL;
+ const char *leftover = NULL;
+ const char *cmd = FIND;
+ int cnt;
+ struct job *r;
+
+ res = sqlite3_prepare_v2(db, cmd, -1, &stmt, &leftover);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_prepare_v2: %s", __func__, sqlite3_errmsg(db));
+ return -1;
+ }
+ if (leftover && leftover < cmd+strlen(cmd)) {
+ warnx("%s: unused text in SQL statement: %s in %s", __func__, leftover, cmd);
+ goto db_find_fail;
+ }
+ res = sqlite3_bind_text(stmt, 1, name, strlen(name), SQLITE_STATIC);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_bind_text(name,1) failed: %s",
+ __func__, sqlite3_errmsg(db));
+ goto db_find_fail;
+ }
+
+ res = sqlite3_step(stmt);
+ if (res != SQLITE_ROW) {
+ if (res == SQLITE_DONE)
+ ret = 0;
+ else
+ warnx("%s: sqlite3_step returned %d, expected SQLITE_DONE(%d) or SQLITE_ROW(%d)",
+ __func__, res, SQLITE_DONE, SQLITE_ROW);
+ goto db_find_fail;
+ }
+
+ if ((cnt = sqlite3_column_count(stmt)) != 3) {
+ warnx("%s: sqlite3_column_count returned %d, expected 3", __func__, cnt);
+ goto db_find_fail;
+ }
+
+ r = job_new(name);
+ r->url = xstrdup((const char *)sqlite3_column_text(stmt, 0));
+ r->date = sqlite3_column_int64(stmt, 1);
+ r->bufsz = sqlite3_column_bytes(stmt, 2);
+ r->buf = xmalloc(r->bufsz);
+ memcpy(r->buf, sqlite3_column_blob(stmt, 2), r->bufsz);
+
+ res = sqlite3_step(stmt);
+ if (res != SQLITE_DONE) {
+ job_free(r);
+ if (res == SQLITE_ROW)
+ warnx("%s: multiple rows returned, should have been one", __func__);
+ else
+ warnx("%s: sqlite3_step returned %d, expected SQLITE_DONE(%d)",
+ __func__, res, SQLITE_DONE);
+ goto db_find_fail;
+ }
+
+ if (retval)
+ *retval = r;
+
+ ret = 1;
+
+db_find_fail:
+ sqlite3_finalize(stmt);
+
+ return ret;
+}
+
+int db_find_all(struct tqh **retval)
+{
+ int res, ret = -1;
+ sqlite3_stmt *stmt = NULL;
+ const char *leftover = NULL;
+ const char *cmd = FIND_ALL;
+ int cnt;
+ struct tqh *r;
+ struct job *j;
+
+ res = sqlite3_prepare_v2(db, cmd, -1, &stmt, &leftover);
+ if (res != SQLITE_OK) {
+ warnx("%s: sqlite3_prepare_v2: %s", __func__, sqlite3_errmsg(db));
+ return -1;
+ }
+ if (leftover && leftover < cmd+strlen(cmd)) {
+ warnx("%s: unused text in SQL statement: %s in %s", __func__, leftover, cmd);
+ goto db_find_all_fail;
+ }
+
+ r = tq_new();
+
+ while (1) {
+ res = sqlite3_step(stmt);
+ if (res != SQLITE_ROW) {
+ if (res == SQLITE_DONE)
+ break;
+ warnx("%s: sqlite3_step returned %d, expected SQLITE_DONE(%d) or SQLITE_ROW(%d)",
+ __func__, res, SQLITE_DONE, SQLITE_ROW);
+ goto db_find_all_fail;
+ }
+
+ if ((cnt = sqlite3_column_count(stmt)) != 4) {
+ warnx("%s: sqlite3_column_count returned %d, expected 4", __func__, cnt);
+ tq_cleanup(r, job_free);
+ goto db_find_all_fail;
+ }
+
+ j = job_new((const char *)sqlite3_column_text(stmt, 0));
+ j->url = xstrdup((const char *)sqlite3_column_text(stmt, 1));
+ j->date = sqlite3_column_int64(stmt, 2);
+ j->bufsz = sqlite3_column_bytes(stmt, 3);
+ j->buf = xmalloc(j->bufsz);
+ memcpy(j->buf, sqlite3_column_blob(stmt, 3), j->bufsz);
+
+ tq_insert_tail(r, tq_elem_new(j));
+ }
+
+ if (tq_empty(r)) {
+ tq_cleanup(r, job_free);
+ free(r);
+ ret = 0;
+ }
+ else {
+ *retval = r;
+ ret = 1;
+ }
+
+db_find_all_fail:
+ sqlite3_finalize(stmt);
+
+ return ret;
+}
+
+void db_cleanup(void)
+{
+ if (sqlite3_close(db) != SQLITE_OK)
+ warnx("%s: sqlite3_close failed: %s", sqlite3_errmsg(db));
+}
diff --git a/surveil/surveil/db.h b/surveil/surveil/db.h
new file mode 100644
index 0000000..44f560a
--- /dev/null
+++ b/surveil/surveil/db.h
@@ -0,0 +1,19 @@
+/* vim: set ts=4 sw=4 sts=4: */
+
+#ifndef SURVEIL_DB_H
+#define SURVEIL_DB_H
+
+#include <sqlite3.h>
+
+struct job;
+struct tqh;
+
+int db_init(const char *);
+int db_insert(const struct job *);
+int db_update(const struct job *);
+int db_delete(const char *);
+int db_find(const char *, struct job **);
+int db_find_all(struct tqh **);
+void db_cleanup(void);
+
+#endif/*!SURVEIL_DB_H*/
diff --git a/surveil/surveil/dgst.c b/surveil/surveil/dgst.c
new file mode 100644
index 0000000..72eb6fa
--- /dev/null
+++ b/surveil/surveil/dgst.c
@@ -0,0 +1,118 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <stdlib.h>
+#include <limits.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <err.h>
+
+#include <openssl/evp.h>
+#include <openssl/bio.h>
+#include <openssl/err.h>
+
+#include "util.h"
+
+#define LONG_STR_SIZE (((sizeof(long)*CHAR_BIT)/3)+2)
+
+static const char xdigits[16] = {
+ "0123456789abcdef"
+};
+
+char *dgst2asc(unsigned char *dgst, size_t len)
+{
+ char *s;
+ size_t i;
+ int c;
+
+ s = xmalloc((len * 2) + 1);
+
+ for (i = 0; i < len; i++) {
+ c = xdigits[(dgst[i]&15)];
+ s[(i*2)+1] = c ? c : '0';
+ dgst[i] >>= 4;
+ c = xdigits[(dgst[i]&15)];
+ s[(i*2)] = c ? c : '0';
+ }
+ s[(i*2)] = '\0';
+
+ return s;
+}
+
+unsigned char *calcdgst(const char *impl, char *buf, ssize_t sz, unsigned *rlen)
+{
+ unsigned int len = 0;
+ EVP_MD_CTX *ctx;
+ EVP_MD *digestimpl;
+ unsigned char *outdigest = NULL;
+
+ ctx = EVP_MD_CTX_new();
+ if (ctx == NULL) {
+ warnx("EVP_MD_CTX_new failed");
+ goto calcdigest_fail0;
+ }
+
+ digestimpl = EVP_MD_fetch(NULL, impl, NULL);
+ if (digestimpl == NULL) {
+ warnx("EVP_MD_fetch failed");
+ goto calcdigest_fail1;
+ }
+
+ if (!EVP_DigestInit_ex(ctx, digestimpl, NULL)) {
+ warnx("EVP_DigestInit_ex failed");
+ goto calcdigest_fail;
+ }
+
+ if (!EVP_DigestUpdate(ctx, buf, sz)) {
+ warnx("EVP_DigestUpdate failed");
+ goto calcdigest_fail;
+ }
+
+ outdigest = OPENSSL_malloc(EVP_MD_get_size(digestimpl));
+ if (outdigest == NULL) {
+ warnx("OPENSSL_malloc failed");
+ goto calcdigest_fail;
+ }
+
+ if (!EVP_DigestFinal_ex(ctx, outdigest, &len)) {
+ warnx("EVP_DigestFinal_ex failed");
+ goto calcdigest_fail;
+ }
+
+calcdigest_fail:
+ EVP_MD_free(digestimpl);
+calcdigest_fail1:
+ EVP_MD_CTX_free(ctx);
+calcdigest_fail0:
+ if (!outdigest)
+ ERR_print_errors_fp(stderr);
+ else
+ *rlen = len;
+
+ return outdigest;
+}
+
+
+void print_digest(EVP_MD *md, void *unused __attribute__((unused)))
+{
+ int sz = EVP_MD_size(md);
+
+ if (sz)
+ printf("%s (%d bit)\n", EVP_MD_get0_name(md), sz * 8);
+}
+
+void print_digests(void)
+{
+ EVP_MD_do_all_provided(NULL, print_digest, NULL);
+}
+
+int digest_is_provided(const char *digest)
+{
+ EVP_MD *md;
+ int ret = 0;
+
+ if ((md = EVP_MD_fetch(NULL, digest, NULL)) != NULL) {
+ EVP_MD_free(md);
+ ret++;
+ }
+
+ return ret;
+}
diff --git a/surveil/surveil/dgst.h b/surveil/surveil/dgst.h
new file mode 100644
index 0000000..e262d1c
--- /dev/null
+++ b/surveil/surveil/dgst.h
@@ -0,0 +1,13 @@
+/* vim: set ts=4 sw=4 sts=4: */
+
+#ifndef SURVEIL_DGST_H
+#define SURVEIL_DGST_H
+
+#define DEFAULT_DIGEST "SHA1"
+
+char *dgst2asc(unsigned char *, size_t);
+unsigned char *calcdgst(const char *, char *, ssize_t, unsigned *);
+void print_digests(void);
+int digest_is_provided(const char *);
+
+#endif/*!SURVEIL_DGST_H*/
diff --git a/surveil/surveil/diff.c b/surveil/surveil/diff.c
new file mode 100644
index 0000000..1d30600
--- /dev/null
+++ b/surveil/surveil/diff.c
@@ -0,0 +1,55 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <stdio.h>
+#include <err.h>
+#include <xdiff/xdiff.h>
+
+#include "buf.h"
+
+/*static int out_line(void *arg __attribute__((unused)), mmbuffer_t *mb, int nmb)
+{
+ int i;
+
+ for (i = 0; i < nmb; i++)
+ printf("%.*s", (int)mb[i].size, mb[i].ptr);
+
+ return 0;
+}*/
+
+static int store_line(void *arg, mmbuffer_t *mb, int nmb)
+{
+ struct buf *b = (struct buf *)arg;
+ int i;
+
+ for (i = 0; i < nmb; i++)
+ if (buf_addstr(b, mb[i].ptr, mb[i].size) == -1)
+ return -1;
+
+ if (buf_addchr(b, '\0') == -1)
+ return -1;
+
+ return 0;
+}
+
+char *get_diff(char *buf1, size_t buf1sz, char *buf2, size_t buf2sz)
+{
+ struct buf b;
+ mmfile_t mf1 = { .ptr = buf1, .size = buf1sz };
+ mmfile_t mf2 = { .ptr = buf2, .size = buf2sz };
+ xpparam_t param = { .flags = 0,
+ .ignore_regex = NULL, .ignore_regex_nr = 0,
+ .anchors = NULL, .anchors_nr = 0 };
+ xdemitconf_t ecfg = { .ctxlen = 0, .interhunkctxlen = 0,
+ .flags = 0,
+ .find_func = NULL, .find_func_priv = NULL,
+ .hunk_func = NULL };
+ xdemitcb_t ecb = { .priv = &b, .out_hunk = NULL, .out_line = store_line };
+
+ buf_init(&b, 1);
+ if (xdl_diff(&mf1, &mf2, &param, &ecfg, &ecb) == -1) {
+ warnx("xdl_diff failed");
+ free(b.ptr);
+ return NULL;
+ }
+
+ return b.ptr;
+}
diff --git a/surveil/surveil/diff.h b/surveil/surveil/diff.h
new file mode 100644
index 0000000..a0e5207
--- /dev/null
+++ b/surveil/surveil/diff.h
@@ -0,0 +1,8 @@
+/* vim: set ts=4 sw=4 sts=4: */
+
+#ifndef SURVEIL_DIFF_H
+#define SURVEIL_DIFF_H
+
+char *get_diff(char *, size_t, char *, size_t);
+
+#endif/*!SURVEIL_DIFF_H*/
diff --git a/surveil/surveil/example.conf b/surveil/surveil/example.conf
new file mode 100644
index 0000000..08a36a7
--- /dev/null
+++ b/surveil/surveil/example.conf
@@ -0,0 +1,22 @@
+
+[traceroute]
+url=https://sourceforge.net/projects/traceroute/rss?path=/traceroute
+pat=traceroute-([[:digit:]]*\.[[:digit:]]*\.[[:digit:]]*)\.tar\.gz
+rpl=\1
+dig=md5
+
+[ awk ]
+url=https://github.com/onetrueawk/awk/commits/master.atom
+pat=<id>.*Commit/([0-9a-fA-F]*)</id>
+rpl=\1
+dig=md5
+
+[ nsjail ]
+url=https://github.com/google/nsjail/commits/master.atom
+pat=<id>.*Commit/([0-9a-fA-F]*)</id>
+rpl=\1
+dig=md5
+
+[ linux ]
+url=https://www.kernel.org
+dig=sha1
diff --git a/surveil/surveil/job.c b/surveil/surveil/job.c
new file mode 100644
index 0000000..f8eb80b
--- /dev/null
+++ b/surveil/surveil/job.c
@@ -0,0 +1,97 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <limits.h>
+#include <err.h>
+#include <queue/tq.h>
+
+#include "dgst.h"
+#include "job.h"
+#include "net.h"
+#include "search.h"
+#include "util.h"
+
+int job_is_name(const void *p, const void *s)
+{
+ const struct job *j;
+ const char *name;
+
+ j = (const struct job *)p;
+ name = (const char *)s;
+
+ if (!j || !j->name || !name)
+ return -1;
+
+ return strcmp(j->name, name);
+}
+
+struct job *job_new(const char *name)
+{
+ struct job *j;
+
+ j = xmalloc(sizeof(struct job));
+ j->name = xstrdup(name);
+ j->url = j->dtype = NULL;
+ j->date = 0;
+ j->npats = 0;
+ j->pats = j->rpls = NULL;
+ j->buf = NULL;
+ j->bufsz = 0;
+
+ return j;
+}
+
+void job_free(void *data)
+{
+ int i;
+ struct job*j = (struct job*)data;
+
+ free(j->buf);
+ for (i = 0; i < j->npats; i++) {
+ free(j->pats[i]);
+ free(j->rpls[i]);
+ }
+ free(j->pats);
+ free(j->rpls);
+ free(j->dtype);
+ free(j->url);
+ free(j->name);
+ free(j);
+}
+
+int run_job(struct job *j)
+{
+ ssize_t sz;
+ char *buf;
+ int i;
+ struct tqh *s;
+
+ if ((sz = net_get_url(j->url, &buf)) == -1)
+ return -1;
+
+ for (i = 0; i < j->npats; i++) {
+ s = search_text(buf, sz, j->pats[i], j->rpls[i]);
+ free(buf);
+ if (!s)
+ return -1;
+
+ sz = get_matches(&buf, s);
+ if (sz == -1)
+ return -1;
+
+ tq_cleanup(s, match_free);
+ free(s);
+ }
+
+ j->date = time(NULL);
+ j->buf = buf;
+ j->bufsz = (size_t)sz;
+
+ return 0;
+}
diff --git a/surveil/surveil/job.h b/surveil/surveil/job.h
new file mode 100644
index 0000000..71e1776
--- /dev/null
+++ b/surveil/surveil/job.h
@@ -0,0 +1,25 @@
+/* vim: set ts=4 sw=4 sts=4: */
+
+#ifndef SURVEIL_JOB_H
+#define SURVEIL_JOB_H
+
+#include <time.h>
+
+struct job {
+ char *name;
+ char *url;
+ time_t date;
+ char *dtype;
+ int npats;
+ char **pats;
+ char **rpls;
+ char *buf;
+ size_t bufsz;
+};
+
+int job_is_name(const void *, const void *);
+struct job *job_new(const char *);
+void job_free(void *);
+int run_job(struct job *);
+
+#endif/*!SURVEIL_JOB_H*/
diff --git a/surveil/surveil/main.c b/surveil/surveil/main.c
new file mode 100644
index 0000000..33cc3c3
--- /dev/null
+++ b/surveil/surveil/main.c
@@ -0,0 +1,489 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <errno.h>
+#include <pwd.h>
+#include <err.h>
+#include <ini.h>
+#include <queue/tq.h>
+
+#include "config.h"
+#include "dgst.h"
+#include "job.h"
+#include "db.h"
+#include "diff.h"
+#include "net.h"
+#include "search.h"
+#include "util.h"
+#include "common.h"
+
+char *progname;
+
+#if INI_HANDLER_LINENO
+static int chandler(void *user, const char *section,
+ const char *name, const char *value, int lineno)
+#else
+static int chandler(void *user, const char *section,
+ const char *name, const char *value)
+#endif
+{
+ struct context *ctx;
+ struct tqe *e;
+ struct job *j;
+
+ ctx = (struct context *)user;
+
+ if (!section || !*section) {
+ if (config_global(ctx->cfg, name, value) == -1)
+ return 0;
+ return 1;
+ }
+
+ section = trimspace(section);
+
+ if (ctx->cfg->nspecified && !name_is_specified(ctx->cfg, section))
+ return 1;
+
+ if (!(e = tq_find_from_data(ctx->jobs, section, job_is_name))) {
+ e = tq_elem_new(job_new(section));
+ if (tq_empty(ctx->jobs))
+ tq_insert_head(ctx->jobs, e);
+ else
+ tq_insert_tail(ctx->jobs, e);
+ }
+
+ j = (struct job *)e->data;
+
+ if (strcasecmp(name, "url") == 0 ||
+ strcasecmp(name, "address") == 0) {
+ if (j->url) {
+ warnx("url for job '%s' redefined, was '%s', new '%s'",
+ j->name, j->url, value);
+ return 0;
+ }
+ j->url = xstrdup(value);
+ }
+ else if (strcasecmp(name, "dig") == 0 ||
+ strcasecmp(name, "dtype") == 0 ||
+ strcasecmp(name, "digest") == 0 ||
+ strcasecmp(name, "digest_type") == 0) {
+ if (j->dtype) {
+ warnx("digest type for job '%s' redefined, was '%s', new '%s'",
+ j->name, j->dtype, value);
+ return 0;
+ }
+ j->dtype = xstrdup(value);
+ }
+ else if (strcasecmp(name, "pat") == 0 ||
+ strcasecmp(name, "pat") == 0) {
+ j->pats = xrealloc(j->pats, sizeof(char *) * (j->npats+1));
+ j->rpls = xrealloc(j->rpls, sizeof(char *) * (j->npats+1));
+ j->pats[j->npats] = xstrdup(value);
+ j->rpls[j->npats] = NULL;
+ j->npats++;
+ }
+ else if (strcasecmp(name, "rpl") == 0 ||
+ strcasecmp(name, "replace") == 0 ||
+ strcasecmp(name, "replacement")== 0) {
+ if (!j->pats[j->npats-1]) {
+ warnx("job '%s': replacement pattern '%s' with no regex pattern",
+ j->name, value);
+ return 0;
+ }
+ if (j->rpls[j->npats-1]) {
+ warnx("job '%s': replacement pattern for regex pattern '%s' redefined, "
+ "was '%s', new '%s'",
+ j->name, value, j->pats[j->npats-1], j->rpls[j->npats-1], value);
+ return 0;
+ }
+ j->rpls[j->npats-1] = xstrdup(value);
+ }
+ else {
+ warnx("job %s: unrecognized key '%s' with value '%s'",
+ j->name, name, value);
+ return 0;
+ }
+
+ return 1;
+}
+
+int job_is_in_jobs(char *name, struct tqh *jobs)
+{
+ if (tq_find_from_data(jobs, name, job_is_name))
+ return 1;
+ return 0;
+}
+
+void prune_data(struct config *cfg, struct tqh *jobs, int stale)
+{
+ size_t i;
+
+ if (cfg->nspecified > 0) {
+ for (i = 0; i < cfg->nspecified; i++) {
+ if (cfg->test) {
+ printf("test: prune %s\n", cfg->specified[i]);
+ continue;
+ }
+ if (db_delete(cfg->specified[i]) == -1) {
+ warnx("purging data for job '%s' failed",
+ cfg->specified[i]);
+ }
+ }
+ }
+ else {
+ struct tqh *indb;
+ struct tqe *e;
+ struct job *j;
+ int res;
+
+ if ((res = db_find_all(&indb)) != 1)
+ return;
+
+ tq_foreach(e, indb) {
+ j = e->data;
+ if (jobs && stale && job_is_in_jobs(j->name, jobs))
+ continue;
+ if (cfg->test) {
+ printf("test: prune %s\n", j->name);
+ continue;
+ }
+ if (db_delete(j->name) == -1) {
+ warnx("pruning job '%s' data failed",
+ j->name);
+ }
+ }
+
+ tq_cleanup(indb, job_free);
+ free(indb);
+ }
+}
+
+int main(int argc, char *argv[])
+{
+ struct context ctx = { 0 };
+ struct config *cfg;
+ struct tqh *jobs;
+ struct tqe *e;
+ struct job *j, *prev;
+ unsigned char *dgst, *prevdgst;
+ unsigned dlen, prevdlen;
+ char *dgstasc, *prevdgstasc;
+ char lasttime[PATH_MAX], thistime[PATH_MAX];
+ char *buf;
+ char *configdir, *configfile, *datadir, *datafile;
+ int eflag, hflag, pflag, Pflag, qflag, tflag, vflag;
+ int res, c, ret = EXIT_FAILURE;
+ size_t i;
+
+ dgstasc = prevdgstasc = NULL;
+ eflag = hflag = Pflag = pflag = qflag = tflag = vflag = 0;
+ configdir = configfile = datadir = datafile = NULL;
+
+ progname = argv[0];
+ for (i = 0; argv[0][i]; i++)
+ if (argv[0][i] == '/')
+ progname = argv[0] + i + 1;
+
+ opterr = 0;
+ while ((c = getopt(argc, argv, ":C:c:D:d:hpPqtv")) != -1) {
+ switch (c) {
+ case 'C':
+ configdir = optarg;
+ break;
+ case 'c':
+ configfile = optarg;
+ break;
+ //case 'd':
+ // print_digests();
+ // exit(EXIT_SUCCESS);
+ case 'D':
+ datadir = optarg;
+ break;
+ case 'd':
+ datafile = optarg;
+ break;
+ case 'h':
+ /* help */
+ hflag++;
+ break;
+ case 'q':
+ /* quiet */
+ qflag++;
+ vflag = 0;
+ break;
+ case 'p':
+ /* prune */
+ pflag++;
+ break;
+ case 'P':
+ /* purge */
+ Pflag++;
+ break;
+ case 't':
+ /* test */
+ tflag++;
+ break;
+ case 'v':
+ /* verbose */
+ vflag++;
+ qflag = 0;
+ break;
+ case ':':
+ fprintf(stderr,
+ "Option -%c requires an operand\n", optopt);
+ eflag++;
+ break;
+ case '?':
+ fprintf(stderr,
+ "Unrecognized option '-%c'\n", optopt);
+ eflag++;
+ break;
+ default:
+ eflag++;
+ break;
+ }
+ }
+
+ if (configdir && configfile) {
+ warnx("-C and -c are mutually exclusive");
+ eflag++;
+ }
+ if (datadir && datafile) {
+ warnx("-D and -d are mutually exclusive");
+ eflag++;
+ }
+
+ if (pflag && Pflag) {
+ warnx("-P (purge) overrides -p (prune)");
+ pflag = 0;
+ }
+
+ if (eflag) {
+ hflag++;
+ ret = EXIT_FAILURE;
+ }
+
+ if (hflag) {
+ fprintf(stderr, "Usage: "
+ "%s [-dhpPqtv] [-C dir|-c file] [-D dir|-d file] [job]...\n", progname);
+ return ret;
+ }
+
+ cfg = config(configdir, configfile, datadir, datafile);
+ if (cfg == NULL)
+ exit(EXIT_FAILURE);
+ ctx.cfg = cfg;
+
+ if (qflag)
+ cfg->quiet = qflag;
+ if (tflag)
+ cfg->test = tflag;
+ if (vflag)
+ cfg->verbose = vflag;
+
+ if (optind < argc) {
+ cfg->nspecified = argc - optind;
+ cfg->specified = xmalloc(sizeof(char *) * cfg->nspecified);
+ for (i = 0; optind < argc; optind++, i++) {
+ cfg->specified[i] = argv[optind];
+ }
+ }
+ else {
+ cfg->nspecified = 0;
+ cfg->specified = NULL;
+ }
+
+ jobs = ctx.jobs = tq_new();
+ ret = ini_parse(cfg->configfile, chandler, &ctx);
+ if (ret < 0 ) {
+ warn("failed reading %s", cfg->configfile);
+ goto main_done;
+ }
+ else if (ret != 0) {
+ warnx("ini_parse(%s) failed at line %d", cfg->configfile, ret);
+ goto main_done;
+ }
+
+ if (cfg->prune && cfg->nspecified)
+ cfg->prune = 0;
+
+ if (pflag) {
+ if (cfg->nspecified > 0) {
+ warnx("-p ignored when jobs are specified");
+ cfg->prune = 0;
+ }
+ else if (config_global(cfg, "prune", "on") == -1) {
+ warnx("failed to set prune setting");
+ goto main_fail;
+ }
+ }
+
+ if (db_init(cfg->datafile) == -1)
+ goto main_done;
+
+ if (Pflag) {
+ prune_data(cfg, NULL, 0);
+ goto main_done;
+ }
+
+ /* Check that all the command line specified jobs actually exist */
+ for (i = 0; i < cfg->nspecified; i++) {
+ int exists = 0;
+ tq_foreach(e, jobs) {
+ j = (struct job *)e->data;
+ if (strcmp(cfg->specified[i], j->name) == 0) {
+ exists = 1;
+ break;
+ }
+ }
+ if (!exists) {
+ warnx("job '%s' not found", cfg->specified[i]);
+ goto main_done;
+ }
+ }
+
+ if (cfg->prune)
+ prune_data(cfg, jobs, 1);
+
+ if (net_init() == -1)
+ goto main_done;
+
+ tq_foreach(e, jobs) {
+ dgst = prevdgst = NULL;
+ dgstasc = prevdgstasc = NULL;
+ j = (struct job *)e->data;
+
+ if (tflag)
+ printf("test: job '%s'\n", j->name);
+
+ /* Do some sanity checks. */
+ if (!j->url) {
+ warnx("job %s: missing url", j->name);
+ eflag++;
+ }
+ if (!j->dtype)
+ j->dtype = DEFAULT_DIGEST;
+ if (!digest_is_provided(j->dtype)) {
+ warnx("job %s: digest %s not provided\n", j->name, j->dtype);
+ eflag++;
+ }
+
+ if (tflag || eflag)
+ continue;
+
+ if (run_job(j) == -1) {
+ warnx("job %s failed", j->name);
+ continue;
+ }
+
+ res = db_find(j->name, &prev);
+ if (res == -1) {
+ warnx("internal db error");
+ eflag++;
+ goto main_fail;
+ }
+ if (res == 0) {
+ if (db_insert(j) == -1) {
+ warnx("db_insert(%s) failed", j->name);
+ eflag++;
+ goto main_fail;
+ }
+ continue;
+ }
+ if (res == 1 && !prev) {
+ warnx("db_find(%s) returned FOUND in database, but return value not set");
+ eflag++;
+ goto main_fail;
+ }
+
+ if (!j->buf) {
+ warnx("data for job %s missing", j->name);
+ eflag++;
+ goto main_fail;
+ }
+ if (!prev->buf) {
+ warnx("data for job %s previous run missing", prev->name);
+ eflag++;
+ goto main_fail;
+ }
+
+ dgst = calcdgst(j->dtype ? j->dtype : DEFAULT_DIGEST, j->buf, j->bufsz, &dlen);
+ if (!dgst) {
+ warnx("failed to derive digest for job %s new data", j->name);
+ eflag++;
+ goto main_fail;
+ }
+ prevdgst = calcdgst(j->dtype ? j->dtype : DEFAULT_DIGEST, prev->buf, prev->bufsz, &prevdlen);
+ if (!dgst) {
+ warnx("failed to derive digest for job %s old data", prev->name);
+ eflag++;
+ goto main_fail;
+ }
+ if (dlen != prevdlen) {
+ warnx("digest lengths for old and new data don't match for job %s", j->name);
+ eflag++;
+ goto main_fail;
+ }
+
+ if (memcmp(dgst, prevdgst, dlen) == 0)
+ goto loop_done;
+
+ dgstasc = dgst2asc(dgst, dlen);
+ prevdgstasc = dgst2asc(prevdgst, prevdlen);
+ if (strftime(lasttime, sizeof(lasttime), "%c", gmtime(&prev->date)) == 0 ||
+ strftime(thistime, sizeof(thistime), "%c", gmtime(&j->date)) == 0)
+ warnx("failed to format date strings");
+
+ if (cfg->verbose != 0) {
+ printf("%s (%s):\n%s: %s\n%s: %s\n\n",
+ j->name, j->url, lasttime, prevdgstasc, thistime, dgstasc);
+ }
+
+ if (db_update(j) == -1) {
+ warnx("db_update(%s) failed", j->name);
+ eflag++;
+ }
+
+ if (cfg->quiet != 0) {
+ buf = get_diff(prev->buf, prev->bufsz, j->buf, j->bufsz);
+ if (buf == NULL) {
+ warnx("failed to get diff with last run for job '%s'", j->name);
+ eflag++;
+ }
+ else {
+ printf("job %s:\n", j->name);
+ printf("prev_dgst=%s\n", prevdgstasc);
+ printf("new_dgst=%s\n", dgstasc);
+ printf("job: %s\n--- last\n+++ this\n%s", j->name, buf);
+ }
+ }
+
+loop_done:
+ free(dgst);
+ free(prevdgst);
+ free(dgstasc);
+ free(prevdgstasc);
+
+ job_free(prev);
+ }
+
+main_fail:
+ net_cleanup();
+ db_cleanup();
+
+ if (!eflag)
+ ret = EXIT_SUCCESS;
+
+main_done:
+ tq_cleanup(jobs, job_free);
+ free(jobs);
+ config_cleanup(cfg);
+
+ return ret;
+}
diff --git a/surveil/surveil/net.c b/surveil/surveil/net.c
new file mode 100644
index 0000000..0f7988d
--- /dev/null
+++ b/surveil/surveil/net.c
@@ -0,0 +1,107 @@
+/* vim: set ts=4 sw=4 ai: */
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <string.h>
+#include <ctype.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <regex.h>
+#include <err.h>
+#include <curl/curl.h>
+
+#include "buf.h"
+#include "util.h"
+
+static CURL *curl;
+
+static size_t write_cb(void *data, size_t size, size_t nmemb, void *clientp)
+{
+ size_t sz = size * nmemb;
+ struct buf *buf = (struct buf *)clientp;
+
+ if (buf_addstr(buf, data, sz) == -1)
+ return 0;
+ if (buf_addchr(buf, '\0') == -1)
+ return 0;
+ return sz;
+}
+
+ssize_t net_get_url(char *url, char **buf)
+{
+ CURLcode res;
+ struct buf b = {
+ .ptr = NULL, .size = 0, .len = 0, .chunksize = 0
+ };
+
+ curl_easy_reset(curl);
+
+ res = curl_easy_setopt(curl, CURLOPT_URL, url);
+ if (res != CURLE_OK) {
+ warnx("curl_easy_seopt(CURLOPT_URL,%s): %s",
+ url, curl_easy_strerror(res));
+ return -1;
+ }
+ res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
+ if (res != CURLE_OK) {
+ warnx("curl_easy_seopt(CURLOPT_WRITEFUNCTION): %s",
+ curl_easy_strerror(res));
+ return -1;
+ }
+
+ res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &b);
+ if (res != CURLE_OK) {
+ warnx("curl_easy_seopt(CURLOPT_WRITEDATA): %s",
+ curl_easy_strerror(res));
+ return -1;
+ }
+
+ if (buf_init(&b, CHNKSZ) == -1) {
+ warnx("failed to initialize recv buffer");
+ return -1;
+ }
+
+ res = curl_easy_perform(curl);
+ if (res != CURLE_OK) {
+ warnx("curl_easy_perform: %s",
+ curl_easy_strerror(res));
+ free(b.ptr);
+ return -1;
+ }
+
+ if (buf_addstr(&b, "\n", 1) == -1 ||
+ buf_addchr(&b, '\0') == -1) {
+ free(b.ptr);
+ return -1;
+ }
+
+ *buf = b.ptr;
+ return b.len;
+}
+
+int net_init(void)
+{
+ CURLcode curlres;
+
+ if ((curlres = curl_global_init(CURL_GLOBAL_DEFAULT)) != 0) {
+ warnx("curl_global_default: %s",
+ curl_easy_strerror(curlres));
+ return -1;
+ }
+
+ if ((curl = curl_easy_init()) == NULL) {
+ warnx("curl_easy_init");
+ curl_global_cleanup();
+ return -1;
+ }
+
+ return 0;
+}
+
+void net_cleanup(void)
+{
+ curl_easy_cleanup(curl);
+ curl_global_cleanup();
+}
diff --git a/surveil/surveil/net.h b/surveil/surveil/net.h
new file mode 100644
index 0000000..f9b8723
--- /dev/null
+++ b/surveil/surveil/net.h
@@ -0,0 +1,11 @@
+/* vim: set ts=4 sw=4 sts=4: */
+#ifndef SURVEIL_NET_H
+#define SURVEIL_NET_H
+
+#include "buf.h"
+
+ssize_t net_get_url(const char *, char **);
+int net_init(void);
+void net_cleanup(void);
+
+#endif/*!SURVEIL_NET_H*/
diff --git a/surveil/surveil/search.c b/surveil/surveil/search.c
new file mode 100644
index 0000000..74b7032
--- /dev/null
+++ b/surveil/surveil/search.c
@@ -0,0 +1,241 @@
+/* vim: set ts=4 sw=4 ai: */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <limits.h>
+#include <ctype.h>
+#include <regex.h>
+#include <err.h>
+#include <queue/tq.h>
+
+#include "buf.h"
+#include "search.h"
+#include "util.h"
+
+struct match *match_new(const char *pat, const char *rpl, int line, const char *str, size_t nsubs, const regmatch_t *subs)
+{
+ struct match *m;
+
+ m = xmalloc(sizeof(struct match));
+ m->pat = xstrdup(pat);
+ if (rpl)
+ m->rpl = xstrdup(rpl);
+ else
+ m->rpl = NULL;
+ m->line = line;
+ m->str = xstrdup(str);
+ m->nsubs = nsubs;
+ if (nsubs) {
+ m->subs = xmalloc(nsubs * sizeof(regmatch_t));
+ memcpy(m->subs, subs, nsubs * sizeof(regmatch_t));
+ }
+ else {
+ m->subs = NULL;
+ }
+
+ return m;
+}
+
+void match_free(void *data)
+{
+ struct match *m;
+
+ m = (struct match *)data;
+
+ free(m->pat);
+ free(m->str);
+ free(m->rpl);
+ if (m->nsubs)
+ free(m->subs);
+ free(m);
+}
+
+struct tqh *search_text(char *buf, size_t sz, char *pat, char *rpl)
+{
+ int i, ret;
+ char *l, *s, *e, *nl;
+ char errbuf[BUFSIZ];
+ size_t nm;
+ regmatch_t *pm;
+ regex_t res;
+ struct tqh *srch;
+ struct tqe *elem;
+ struct match *m;
+
+ /* XXX consider REG_NEWLINE for cflags */
+ if ((ret = regcomp(&res, pat, REG_EXTENDED)) != 0) {
+ regerror(ret, &res, errbuf, sizeof(errbuf));
+ warnx("%s: %s\n", pat, errbuf);
+ return NULL;
+ }
+
+ nm = res.re_nsub + 1;
+ pm = xmalloc(nm * sizeof(regmatch_t));
+
+ srch = tq_new();
+
+ for (l = buf, e = l+sz, i = 1;l && *l && l < e; i++) {
+ if ((nl = strchr(l, '\n')) != NULL)
+ *nl = '\0';
+
+ ret = regexec(&res, l, nm, pm, 0);
+ if (ret == REG_NOMATCH)
+ goto skip;
+ m = match_new(pat, rpl, i, l, nm, pm);
+ elem = tq_elem_new(m);
+ if (tq_empty(srch))
+ tq_insert_head(srch, elem);
+ else
+ tq_insert_tail(srch, elem);
+
+ /* XXX Should this be rm_so+1 to start just after
+ * the start of the last match? Seems like it depends
+ * on whether overlapping matches are ok. */
+ /*s = l + pm[0].rm_so + 1;*/
+ s = l + pm[0].rm_eo;
+ while (s < e) {
+ ret = regexec(&res, s, nm, pm, REG_NOTBOL);
+ if (ret == REG_NOMATCH)
+ break;
+ m = match_new(pat, rpl, i, l, nm, pm);
+ elem = tq_elem_new(m);
+ if (tq_empty(srch))
+ tq_insert_head(srch, elem);
+ else
+ tq_insert_tail(srch, elem);
+
+ s += pm[0].rm_eo;
+ /* XXX See above about overlapping matches. */
+ }
+
+skip:
+ if (nl)
+ l = nl+1;
+ else
+ l = e;
+ }
+
+ free(pm);
+ regfree(&res);
+
+ return srch;
+}
+
+/* derived from NetBSD's src/lib/libc/regex/regsub.c: regasub() */
+ssize_t regsub(char **buf, const char *sub, const regmatch_t *pm, const char *str)
+{
+ ssize_t i;
+ char c;
+ struct buf b = {
+ .ptr = NULL, .len = 0, .size = 0, .chunksize = 0
+ };
+
+ if (buf_init(&b, CHNKSZ) == -1)
+ return -1;
+
+ while ((c = *sub++) != '\0') {
+ switch (c) {
+ case '&':
+ i = 0;
+ break;
+ case '\\':
+ if (isdigit((unsigned char)*sub))
+ i = *sub++ - '0';
+ else
+ i = -1;
+ break;
+ default:
+ i = -1;
+ break;
+ }
+
+ if (i == -1) {
+ if (c == '\\' && (*sub == '\\' || *sub == '&'))
+ c = *sub++;
+ if (buf_addchr(&b, c) == -1)
+ goto regsub_fail;
+ }
+ else if (pm[i].rm_so != -1 && pm[i].rm_eo != -1) {
+ size_t l = (size_t)(pm[i].rm_eo - pm[i].rm_so);
+ if (buf_addstr(&b, str + pm[i].rm_so, l) == -1)
+ goto regsub_fail;
+ }
+ if (b.len > b.size)
+ goto regsub_fail;
+ }
+
+ if (buf_addchr(&b, '\n') == -1 ||
+ buf_addchr(&b, '\0') == -1 ||
+ b.len > b.size)
+ goto regsub_fail;
+
+ *buf = b.ptr;
+ return b.len;
+
+regsub_fail:
+ free(b.ptr);
+ return -1;
+}
+
+ssize_t get_matches(char **buf, struct tqh *s)
+{
+ ssize_t l;
+ char *rpl;
+ struct tqe *e;
+ struct match *m;
+ struct buf b = {
+ .ptr = NULL, .len = 0, .size = 0, .chunksize = 0
+ };
+
+ if (buf_init(&b, 1) == -1)
+ return -1;
+
+ tq_foreach(e, s) {
+ m = (struct match *)e->data;
+ if (!m->rpl)
+ buf_addstr(&b, m->str + m->subs[0].rm_so,
+ (int)(m->subs[0].rm_eo - m->subs[0].rm_so));
+ else {
+ if ((l = regsub(&rpl, m->rpl, m->subs, m->str)) == -1) {
+ warnx("regsub failed\n");
+ continue;
+ }
+ if (buf_addstr(&b, rpl, l) == -1)
+ goto get_matches_fail;
+ free(rpl);
+ }
+ }
+
+ if (buf_addchr(&b, '\n') == -1 ||
+ buf_addchr(&b, '\0') == -1 ||
+ b.len > b.size)
+ goto get_matches_fail;
+
+ *buf = b.ptr;
+ return b.len;
+
+get_matches_fail:
+ free(b.ptr);
+ return -1;
+}
+
+#if 0
+void print_match(struct match *m)
+{
+ char *rpl;
+
+ if (!m->rpl)
+ printf("'%.*s'\n",
+ (int)(m->subs[0].rm_eo - m->subs[0].rm_so),
+ m->str + m->subs[0].rm_so);
+
+ if (regsub(&rpl, m->rpl, m->subs, m->str) != -1) {
+ printf("%s\n", rpl);
+ free(rpl);
+ }
+ else {
+ warnx("regsub failed");
+ }
+}
+#endif
diff --git a/surveil/surveil/search.h b/surveil/surveil/search.h
new file mode 100644
index 0000000..e1ff993
--- /dev/null
+++ b/surveil/surveil/search.h
@@ -0,0 +1,26 @@
+/* vim: set ts=4 sw=4 sts=4: */
+
+#ifndef SURVEIL_SEARCH_H
+#define SURVEIL_SEARCH_H
+
+#include <regex.h>
+#include <queue/tq.h>
+
+struct match {
+ char *pat;
+ char *rpl;
+ int line;
+ char *str;
+ size_t nsubs;
+ regmatch_t *subs;
+};
+
+
+struct match *match_new(const char *, const char *, int, const char *, size_t, const regmatch_t *);
+void match_free(void *);
+struct tqh *search_text(char *, size_t, char *, char *);
+ssize_t regsub(char **, const char *, const regmatch_t *, const char *);
+ssize_t get_matches(char **, struct tqh *);
+void print_match(struct match *);
+
+#endif/*!SURVEIL_SEARCH_H*/
diff --git a/surveil/surveil/surveil.1 b/surveil/surveil/surveil.1
new file mode 100644
index 0000000..bd3d1ee
--- /dev/null
+++ b/surveil/surveil/surveil.1
@@ -0,0 +1,82 @@
+.Dd August 25, 2023
+.Dt surveil 1
+.Os
+.Sh NAME
+.Nm surveil
+.Nd track changes to remote files
+.Sh SYNOPSIS
+.Nm
+.Op Fl hpqtv
+.Op Fl C Ar dir | Fl c Ar file
+.Op Fl D Ar dir | Fl d Ar file
+.Op Ar jobname ...
+.Sh DESCRIPTION
+The
+.Nm
+command tracks changes to remote files.
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl C Ar dir
+Use
+.Ar dir
+to find configuration
+.It Fl c Ar file
+Use
+.Ar file
+for configuration
+.It Fl D Ar dir
+Use
+.Ar dir
+to store data
+.It Fl d Ar file
+Use
+.Ar file
+to store data
+.It Fl h
+Print help message
+.\".It Fl p
+.\"Print available digests
+.It Fl p
+Clean out (prune) data for jobs not in config.
+.It Fl P
+Clean out (purge) all data, or for command line specified jobs.
+.It Fl q
+Turns on quiet output
+.It Fl t
+Test configuration and exit
+.It Fl v
+Turns on verbose output
+.El
+.Sh ENVIRONMENT
+If the following environment variables exist, they will be used by
+.Nm :
+.Bl -tag -width EDITOR
+.It Ev HOME
+.It Ev XDG_CONFIG_HOME
+.It Ev XDG_DATA_HOME
+.El
+.Sh FILES
+These directories will be searched, in listed order, to load configuration:
+.Bl -tag -width Pa
+.It Pa $XDG_CONFIG_HOME/surveil/
+.It Pa $HOME/.config/surveil/
+.It Pa $HOME/.surveil/
+.El
+
+.Bl -tag -width Pa
+These directories will be searched, in listed order, to store data:
+.It Pa $XDG_DATA_HOME/surveil/surveil.dat
+.It Pa $HOME/.local/share/surveil/surveil.dat
+.It Pa $HOME/.surveil/surveil.dat
+.El
+.\".Sh EXIT STATUS
+.\" Actually, currently, the exit status is 0 on success and -1 on error
+\".Ex -std
+.\".Sh EXAMPLES
+.\".Sh DIAGNOSTICS
+.Sh SEE ALSO
+.Xr openssl-dgst 1 ,
+.Xr regex 7
+.\".Sh AUTHORS
+.\".Sh CAVEATS
+\.".Sh BUGS
diff --git a/surveil/surveil/util.c b/surveil/surveil/util.c
new file mode 100644
index 0000000..c0ac090
--- /dev/null
+++ b/surveil/surveil/util.c
@@ -0,0 +1,219 @@
+/* vim: set ts=4 sw=4 ai: */
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <string.h>
+#include <ctype.h>
+#include <limits.h>
+#include <errno.h>
+#include <err.h>
+
+void *xmalloc(size_t sz)
+{
+ void *p;
+
+ p = malloc(sz);
+ if (!p)
+ err(EXIT_FAILURE, "malloc(%u)", sz);
+
+ return p;
+}
+
+void *xrealloc(void *x, size_t sz)
+{
+ void *p;
+
+ p = realloc(x, sz);
+ if (!p)
+ err(EXIT_FAILURE, "realloc(%x,%u)", x, sz);
+
+ return p;
+}
+
+char *xstrdup(const char *s)
+{
+ char *p;
+
+ p = strdup(s);
+ if (!p)
+ err(EXIT_FAILURE, "strdup(%s)", s);
+
+ return p;
+}
+
+char *trimspace(char *s)
+{
+ char *p;
+
+ /* skip leading space */
+ while (*s && isspace(*s))
+ s++;
+ /* find end of string */
+ for (p = s; *(p+1); p++) ;
+ /* trim trailing space */
+ while (isspace(*p))
+ *p-- = '\0';
+
+ return s;
+}
+
+
+ssize_t readfile(const char *filename, char **rbuf)
+{
+ char *buf;
+ off_t sz;
+ ssize_t offset;
+ ssize_t n, ret = -1;
+ int fd;
+
+ if ((fd = open(filename, O_RDONLY)) == -1) {
+ warn("%s", filename);
+ return -1;
+ }
+
+#define CHUNKSZ (1024*64)
+ offset = 0;
+ sz = 0;
+ buf = NULL;
+ while (1) {
+ if (sz == offset) {
+ sz += CHUNKSZ;
+ buf = xrealloc(buf, sz);
+ }
+ n = read(fd, buf + (int)offset, sz - offset);
+ if (n == -1 && errno != EINTR) {
+ warn("read failed");
+ free(buf);
+ break;
+ }
+ if (n == 0) {
+ *rbuf = buf;
+ ret = offset;
+ break;
+ }
+ offset += n;
+ }
+
+ close(fd);
+
+ return ret;
+}
+
+ssize_t writefile(const char *filename, const char *buf, size_t len, int excl)
+{
+ ssize_t n;
+ size_t left, total;
+ int fd, flags;
+ const char *p;
+
+ flags = O_WRONLY | O_CREAT;
+ if (excl)
+ flags |= O_EXCL;
+
+ if ((fd = open(filename, flags, 0600)) == -1) {
+ warn("%s", filename);
+ return -1;
+ }
+
+ total = 0;
+ left = len;
+ p = buf;
+ while (left > 0) {
+ if ((n = write(fd, p, len)) == -1) {
+ if (errno != EINTR) {
+ warn("write(%s)", filename);
+ close(fd);
+ return -1;
+ }
+ }
+ if (n == 0)
+ break;
+ total += n;
+ left -= n;
+ p += n;
+ }
+
+ close(fd);
+
+ return total;
+}
+
+int s2l(const char *s, long *ret)
+{
+ char *endp;
+ long val;
+
+ if (!s || !*s) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ errno = 0;
+ val = strtol(s, &endp, 10);
+ if (errno)
+ return -1;
+ if (!endp || *endp != '\0') {
+ errno = EINVAL;
+ return -1;
+ }
+
+ *ret = val;
+ return 0;
+}
+
+int s2i(const char *s, int *ret)
+{
+ long val;
+
+ if (s2l(s, &val) == -1)
+ return -1;
+
+ if (val > INT_MAX || val < INT_MIN) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *ret = val;
+ return 0;
+}
+
+int s2ul(const char *s, unsigned long *ret)
+{
+ char *endp;
+ unsigned long val;
+
+ if (!s || !*s) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ errno = 0;
+ val = strtoul(s, &endp, 10);
+ if (errno)
+ return -1;
+ if (!endp || *endp != '\0') {
+ errno = EINVAL;
+ return -1;
+ }
+
+ *ret = val;
+ return 0;
+}
+
+int s2u (const char *s, unsigned *ret)
+{
+ unsigned long val;
+
+ if (s2ul(s, &val) == -1)
+ return -1;
+
+ if (val > UINT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *ret = val;
+ return 0;
+}
diff --git a/surveil/surveil/util.h b/surveil/surveil/util.h
new file mode 100644
index 0000000..a6f903b
--- /dev/null
+++ b/surveil/surveil/util.h
@@ -0,0 +1,18 @@
+/* vim: set ts=4 sw=4 ai: */
+#ifndef SURVEIL_UTIL_H
+#define SURVEIL_UTIL_H
+
+#include <sys/types.h>
+
+void *xmalloc(size_t);
+void *xrealloc(void *, size_t);
+char *xstrdup(const char *);
+char *trimspace(const char *);
+ssize_t readfile(const char *, char **);
+ssize_t writefile(const char *, const char *, size_t, int);
+int s2ul(const char *, unsigned long *);
+int s2l(const char *, long *);
+int s2u(const char *, unsigned *);
+int s2i(const char *, int *);
+
+#endif/*!SURVEIL_UTIL_H*/