aboutsummaryrefslogtreecommitdiff
path: root/src/tax_table.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/tax_table.c')
-rw-r--r--src/tax_table.c745
1 files changed, 745 insertions, 0 deletions
diff --git a/src/tax_table.c b/src/tax_table.c
new file mode 100644
index 0000000..1cf40c1
--- /dev/null
+++ b/src/tax_table.c
@@ -0,0 +1,745 @@
+#include "tax_table.h"
+
+#include <errno.h>
+#include <limits.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+
+#include "db.h"
+#include "version.h"
+
+#define TT_LINE_LEN 49
+#define TT_TIMEOUT_SEC 30
+#define TT_MAX_REDIRECTS 5
+#define TT_TABLE_FILE "allmanna-tabeller-manad.txt"
+#define TT_PAGE_URL \
+ "https://www.skatteverket.se/foretag/arbetsgivare/" \
+ "arbetsgivaravgifterochskatteavdrag/skattetabeller/" \
+ "specialversionerforprogramforetagmfl.4.319dc1451507f2f99e86ee.html"
+
+static void set_err(char **err, const char *fmt, ...)
+ __attribute__((format(printf, 2, 3)));
+
+static void set_err(char **err, const char *fmt, ...)
+{
+ if (!err || *err)
+ return;
+ char buf[512];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(buf, sizeof buf, fmt, ap);
+ va_end(ap);
+ *err = xstrdup(buf);
+}
+
+/* ------------------------------------------------------------------ */
+/* parser */
+/* ------------------------------------------------------------------ */
+
+static int parse_int_field(const char *s, int len, int64_t *v, int *empty)
+{
+ int i = 0, j = len;
+ while (i < j && s[i] == ' ')
+ i++;
+ while (j > i && s[j - 1] == ' ')
+ j--;
+ *empty = i == j;
+ if (*empty) {
+ *v = 0;
+ return 0;
+ }
+ int64_t x = 0;
+ for (int k = i; k < j; k++) {
+ if (s[k] < '0' || s[k] > '9')
+ return -1;
+ x = x * 10 + (s[k] - '0');
+ }
+ *v = x;
+ return 0;
+}
+
+int tax_table_parse(const unsigned char *data, size_t len,
+ struct tax_row **out, size_t *out_n, char **err)
+{
+ *out = NULL;
+ *out_n = 0;
+ if (len >= 3 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf) {
+ data += 3;
+ len -= 3;
+ }
+ struct tax_row *rows = NULL;
+ size_t n = 0, cap = 0;
+ size_t pos = 0;
+ int line_no = 0;
+ while (pos < len) {
+ const char *line = (const char *)data + pos;
+ size_t end = pos;
+ while (end < len && data[end] != '\n')
+ end++;
+ size_t llen = end - pos;
+ pos = end + 1;
+ line_no++;
+ while (llen > 0 && (line[llen - 1] == '\r' ||
+ line[llen - 1] == ' ' || line[llen - 1] == '\t'))
+ llen--;
+ if (llen == 0)
+ continue;
+ if (llen != TT_LINE_LEN) {
+ set_err(err, "line %d: expected %d characters, got %zu", line_no,
+ TT_LINE_LEN, llen);
+ goto bad;
+ }
+ if (line[0] != '3' || line[1] != '0' ||
+ (line[2] != 'B' && line[2] != '%')) {
+ set_err(err, "line %d: not a 30B/30%% record", line_no);
+ goto bad;
+ }
+ int table_no = (line[3] - '0') * 10 + (line[4] - '0');
+ if (table_no < 29 || table_no > 42) {
+ set_err(err, "line %d: table %d is out of range", line_no,
+ table_no);
+ goto bad;
+ }
+ int is_pct = line[2] == '%';
+ int64_t from_kr = 0, to_kr = 0, col_kr = 0;
+ int empty = 0;
+ if (parse_int_field(line + 5, 7, &from_kr, &empty) != 0 || empty) {
+ set_err(err, "line %d: bad income from", line_no);
+ goto bad;
+ }
+ if (parse_int_field(line + 12, 7, &to_kr, &empty) != 0) {
+ set_err(err, "line %d: bad income to", line_no);
+ goto bad;
+ }
+ int has_to = !empty;
+ if (!has_to && !is_pct) {
+ set_err(err, "line %d: B record needs an income to", line_no);
+ goto bad;
+ }
+ if (has_to && to_kr < from_kr) {
+ set_err(err, "line %d: income range is reversed", line_no);
+ goto bad;
+ }
+ for (int c = 0; c < 6; c++) {
+ if (parse_int_field(line + 19 + 5 * c, 5, &col_kr, &empty) != 0 ||
+ empty) {
+ set_err(err, "line %d: bad column %d", line_no, c + 1);
+ goto bad;
+ }
+ if (n == cap) {
+ cap = cap ? cap * 2 : 1024;
+ rows = xrealloc(rows, cap * sizeof *rows);
+ }
+ struct tax_row *row = &rows[n++];
+ row->table_no = table_no;
+ row->column_no = c + 1;
+ row->income_from_ore = from_kr * 100;
+ row->income_to_ore = has_to ? to_kr * 100 : -1;
+ row->tax_ore = is_pct ? 0 : col_kr * 100;
+ row->pct = is_pct ? col_kr * 100 : 0;
+ row->is_pct = is_pct;
+ }
+ }
+ if (n == 0) {
+ set_err(err, "no table records found");
+ goto bad;
+ }
+ *out = rows;
+ *out_n = n;
+ return 0;
+
+bad:
+ free(rows);
+ return -1;
+}
+
+/* ------------------------------------------------------------------ */
+/* storage */
+/* ------------------------------------------------------------------ */
+
+int tax_table_store(sqlite3 *db, int year, const struct tax_row *rows,
+ size_t n, const char *source_url,
+ const unsigned char sha256[32], const char *fetched_at,
+ char **err)
+{
+ char *del = sqlite3_mprintf("DELETE FROM tax_tables WHERE in_year=%d",
+ year);
+ if (!del) {
+ set_err(err, "out of memory");
+ return -1;
+ }
+ int drc = db_exec(db, del, err);
+ sqlite3_free(del);
+ if (drc != 0)
+ return -1;
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO tax_tables(in_year,table_no,column_no,income_from_ore,"
+ "income_to_ore,tax_ore,pct)"
+ " VALUES(?1,?2,?3,?4,?5,?6,?7)",
+ -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "database error: %s", sqlite3_errmsg(db));
+ return -1;
+ }
+ for (size_t i = 0; i < n; i++) {
+ sqlite3_bind_int(st, 1, year);
+ sqlite3_bind_int(st, 2, rows[i].table_no);
+ sqlite3_bind_int(st, 3, rows[i].column_no);
+ sqlite3_bind_int64(st, 4, rows[i].income_from_ore);
+ if (rows[i].income_to_ore < 0)
+ sqlite3_bind_null(st, 5);
+ else
+ sqlite3_bind_int64(st, 5, rows[i].income_to_ore);
+ sqlite3_bind_int64(st, 6, rows[i].tax_ore);
+ if (rows[i].is_pct)
+ sqlite3_bind_int64(st, 7, rows[i].pct);
+ else
+ sqlite3_bind_null(st, 7);
+ int rc = sqlite3_step(st);
+ sqlite3_reset(st);
+ sqlite3_clear_bindings(st);
+ if (rc != SQLITE_DONE) {
+ set_err(err, "could not store row %zu: %s", i + 1,
+ sqlite3_errmsg(db));
+ sqlite3_finalize(st);
+ return -1;
+ }
+ }
+ sqlite3_finalize(st);
+ if (sqlite3_prepare_v2(
+ db,
+ "INSERT INTO tax_table_meta(in_year,source_url,sha256,fetched_at)"
+ " VALUES(?1,?2,?3,?4)"
+ " ON CONFLICT(in_year) DO UPDATE SET"
+ " source_url=excluded.source_url, sha256=excluded.sha256,"
+ " fetched_at=excluded.fetched_at",
+ -1, &st, NULL) != SQLITE_OK) {
+ set_err(err, "database error: %s", sqlite3_errmsg(db));
+ return -1;
+ }
+ sqlite3_bind_int(st, 1, year);
+ sqlite3_bind_text(st, 2, source_url, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_blob(st, 3, sha256, 32, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, fetched_at, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE) {
+ set_err(err, "could not store metadata: %s", sqlite3_errmsg(db));
+ return -1;
+ }
+ return 0;
+}
+
+int tax_table_lookup(sqlite3 *db, int year, int table_no, int column_no,
+ int64_t gross_ore, int64_t *tax_ore)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT MAX(income_to_ore) FROM tax_tables"
+ " WHERE in_year=?1 AND table_no=?2 AND column_no=?3"
+ " AND pct IS NULL",
+ -1, &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int(st, 1, year);
+ sqlite3_bind_int(st, 2, table_no);
+ sqlite3_bind_int(st, 3, column_no);
+ int step = sqlite3_step(st);
+ if (step != SQLITE_ROW ||
+ sqlite3_column_type(st, 0) == SQLITE_NULL) {
+ sqlite3_finalize(st);
+ return 2;
+ }
+ int64_t top = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ if (gross_ore > top)
+ return 1;
+ if (sqlite3_prepare_v2(
+ db,
+ "SELECT tax_ore FROM tax_tables"
+ " WHERE in_year=?1 AND table_no=?2 AND column_no=?3"
+ " AND pct IS NULL AND income_from_ore<=?4 AND income_to_ore>=?4",
+ -1, &st, NULL) != SQLITE_OK)
+ return -1;
+ sqlite3_bind_int(st, 1, year);
+ sqlite3_bind_int(st, 2, table_no);
+ sqlite3_bind_int(st, 3, column_no);
+ sqlite3_bind_int64(st, 4, gross_ore);
+ step = sqlite3_step(st);
+ if (step != SQLITE_ROW) {
+ sqlite3_finalize(st);
+ return 2;
+ }
+ *tax_ore = sqlite3_column_int64(st, 0);
+ sqlite3_finalize(st);
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* HTTPS GET */
+/* ------------------------------------------------------------------ */
+
+struct tt_url {
+ char host[256];
+ char path[2048];
+ int port;
+};
+
+static int tt_url_parse(const char *url, struct tt_url *u, char **err)
+{
+ if (strncmp(url, "https://", 8) != 0) {
+ set_err(err, "not an https URL: %.120s", url);
+ return -1;
+ }
+ const char *p = url + 8;
+ const char *slash = strchr(p, '/');
+ const char *host_end = slash ? slash : p + strlen(p);
+ const char *colon = memchr(p, ':', (size_t)(host_end - p));
+ size_t hl = colon ? (size_t)(colon - p) : (size_t)(host_end - p);
+ if (hl == 0 || hl >= sizeof u->host) {
+ set_err(err, "bad host in URL: %.120s", url);
+ return -1;
+ }
+ memcpy(u->host, p, hl);
+ u->host[hl] = '\0';
+ u->port = 443;
+ if (colon) {
+ long port = strtol(colon + 1, NULL, 10);
+ if (port < 1 || port > 65535) {
+ set_err(err, "bad port in URL: %.120s", url);
+ return -1;
+ }
+ u->port = (int)port;
+ }
+ snprintf(u->path, sizeof u->path, "%s", slash ? slash : "/");
+ return 0;
+}
+
+static int tt_tcp_connect(const struct tt_url *u, char **err)
+{
+ char portstr[16];
+ snprintf(portstr, sizeof portstr, "%d", u->port);
+ struct addrinfo hints;
+ memset(&hints, 0, sizeof hints);
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ struct addrinfo *res = NULL;
+ int gai = getaddrinfo(u->host, portstr, &hints, &res);
+ if (gai != 0) {
+ set_err(err, "cannot resolve %s: %s", u->host, gai_strerror(gai));
+ return -1;
+ }
+ int last = 0;
+ for (struct addrinfo *ai = res; ai; ai = ai->ai_next) {
+ int fd = socket(ai->ai_family, ai->ai_socktype | SOCK_CLOEXEC,
+ ai->ai_protocol);
+ if (fd < 0) {
+ last = errno;
+ continue;
+ }
+ struct timeval tv;
+ tv.tv_sec = TT_TIMEOUT_SEC;
+ tv.tv_usec = 0;
+ (void)setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
+ (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
+ if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0) {
+ freeaddrinfo(res);
+ return fd;
+ }
+ last = errno;
+ close(fd);
+ }
+ freeaddrinfo(res);
+ set_err(err, "cannot connect to %s:%d: %s", u->host, u->port,
+ last ? strerror(last) : "unknown error");
+ return -1;
+}
+
+static int tt_tls_start(SSL_CTX *ctx, SSL **ssl, int fd, const char *host,
+ char **err)
+{
+ *ssl = SSL_new(ctx);
+ if (!*ssl) {
+ set_err(err, "cannot create TLS connection");
+ return -1;
+ }
+ if (SSL_set_fd(*ssl, fd) != 1 ||
+ SSL_set_tlsext_host_name(*ssl, host) != 1 ||
+ SSL_set1_host(*ssl, host) != 1) {
+ set_err(err, "cannot set up TLS connection");
+ return -1;
+ }
+ if (SSL_connect(*ssl) != 1) {
+ long vr = SSL_get_verify_result(*ssl);
+ unsigned long ec = ERR_get_error();
+ if (vr != X509_V_OK)
+ set_err(err, "TLS certificate verification failed: %s",
+ X509_verify_cert_error_string(vr));
+ else if (ec)
+ set_err(err, "TLS handshake failed: %s",
+ ERR_error_string(ec, NULL));
+ else
+ set_err(err, "TLS handshake failed");
+ return -1;
+ }
+ if (SSL_get_verify_result(*ssl) != X509_V_OK) {
+ set_err(err, "TLS certificate verification failed: %s",
+ X509_verify_cert_error_string(SSL_get_verify_result(*ssl)));
+ return -1;
+ }
+ return 0;
+}
+
+static int tt_write(SSL *ssl, const void *data, size_t n, char **err)
+{
+ const unsigned char *p = data;
+ while (n > 0) {
+ int chunk = n > (size_t)INT_MAX ? INT_MAX : (int)n;
+ int w = SSL_write(ssl, p, chunk);
+ if (w <= 0) {
+ int e = SSL_get_error(ssl, w);
+ if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE)
+ continue;
+ set_err(err, "TLS write failed");
+ return -1;
+ }
+ p += w;
+ n -= (size_t)w;
+ }
+ return 0;
+}
+
+static int tt_read_all(SSL *ssl, struct buf *out, char **err)
+{
+ for (;;) {
+ unsigned char chunk[65536];
+ int r = SSL_read(ssl, chunk, (int)sizeof chunk);
+ if (r > 0) {
+ buf_append(out, chunk, (size_t)r);
+ continue;
+ }
+ int e = SSL_get_error(ssl, r);
+ if (e == SSL_ERROR_ZERO_RETURN)
+ return 0;
+ if (e == SSL_ERROR_WANT_READ || e == SSL_ERROR_WANT_WRITE)
+ continue;
+ if (e == SSL_ERROR_SYSCALL && r == 0)
+ return 0;
+#ifdef SSL_R_UNEXPECTED_EOF_WHILE_READING
+ if (e == SSL_ERROR_SSL) {
+ unsigned long ec = ERR_peek_last_error();
+ if (ec &&
+ ERR_GET_REASON(ec) == SSL_R_UNEXPECTED_EOF_WHILE_READING)
+ return 0;
+ }
+#endif
+ if (e == SSL_ERROR_SYSCALL &&
+ (errno == EAGAIN || errno == EWOULDBLOCK)) {
+ set_err(err, "receive timed out");
+ return -1;
+ }
+ unsigned long ec = ERR_get_error();
+ if (ec)
+ set_err(err, "TLS read failed: %s", ERR_error_string(ec, NULL));
+ else
+ set_err(err, "TLS read failed");
+ return -1;
+ }
+}
+
+/* One GET with Connection: close; returns status, Location (or NULL) and
+ the response body. */
+static int tt_get_once(const struct tt_url *u, struct buf *body, int *status,
+ char **location, char **err)
+{
+ int fd = tt_tcp_connect(u, err);
+ if (fd < 0)
+ return -1;
+ SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
+ SSL *ssl = NULL;
+ int rc = -1;
+ if (!ctx) {
+ set_err(err, "cannot create TLS context");
+ goto done;
+ }
+ SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
+ SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION | SSL_OP_NO_RENEGOTIATION);
+ SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
+ if (SSL_CTX_set_default_verify_paths(ctx) != 1) {
+ set_err(err, "cannot load system CA certificates");
+ goto done;
+ }
+ if (tt_tls_start(ctx, &ssl, fd, u->host, err) != 0)
+ goto done;
+
+ struct buf req;
+ buf_init(&req);
+ static const char head_start[] = "GET ";
+ static const char head_mid[] = " HTTP/1.1\r\nHost: ";
+ static const char head_end[] = "\r\nUser-Agent: bokf/" BOKF_VERSION
+ "\r\nAccept: */*\r\nConnection: close\r\n\r\n";
+ buf_append(&req, head_start, sizeof head_start - 1);
+ buf_append(&req, u->path, strlen(u->path));
+ buf_append(&req, head_mid, sizeof head_mid - 1);
+ buf_append(&req, u->host, strlen(u->host));
+ buf_append(&req, head_end, sizeof head_end - 1);
+ int ok = tt_write(ssl, req.p, req.len, err);
+ buf_free(&req);
+ if (ok != 0)
+ goto done;
+
+ struct buf raw;
+ buf_init(&raw);
+ if (tt_read_all(ssl, &raw, err) != 0) {
+ buf_free(&raw);
+ goto done;
+ }
+ size_t hdr_end = 0;
+ int found = 0;
+ for (size_t i = 0; i + 3 < raw.len; i++) {
+ if (raw.p[i] == '\r' && raw.p[i + 1] == '\n' &&
+ raw.p[i + 2] == '\r' && raw.p[i + 3] == '\n') {
+ hdr_end = i + 4;
+ found = 1;
+ break;
+ }
+ }
+ if (!found) {
+ set_err(err, "malformed HTTP response");
+ buf_free(&raw);
+ goto done;
+ }
+ *status = 0;
+ if (raw.len >= 12 && memcmp(raw.p, "HTTP/", 5) == 0)
+ *status = atoi((const char *)raw.p + 9);
+ char line[1024];
+ size_t p = 0;
+ int first = 1;
+ while (p < hdr_end) {
+ size_t e = p;
+ while (e < hdr_end && raw.p[e] != '\n')
+ e++;
+ size_t n = e - p;
+ if (n > 0 && raw.p[e - 1] == '\r')
+ n--;
+ if (n >= sizeof line)
+ n = sizeof line - 1;
+ memcpy(line, raw.p + p, n);
+ line[n] = '\0';
+ if (!first && n >= 9 && strncasecmp(line, "location:", 9) == 0) {
+ const char *v = line + 9;
+ while (*v == ' ' || *v == '\t')
+ v++;
+ free(*location);
+ *location = xstrdup(v);
+ }
+ first = 0;
+ p = e + 1;
+ }
+ for (size_t i = hdr_end; i < raw.len; i++)
+ buf_append(body, raw.p + i, 1);
+ buf_free(&raw);
+ rc = 0;
+
+done:
+ if (ssl) {
+ SSL_shutdown(ssl);
+ SSL_free(ssl);
+ }
+ if (ctx)
+ SSL_CTX_free(ctx);
+ close(fd);
+ return rc;
+}
+
+static int tt_is_absolute(const char *u)
+{
+ return strncmp(u, "https://", 8) == 0 ||
+ strncmp(u, "http://", 7) == 0;
+}
+
+static char *tt_resolve(const char *base, const char *location, char **err)
+{
+ if (tt_is_absolute(location))
+ return xstrdup(location);
+ struct tt_url u;
+ if (tt_url_parse(base, &u, err) != 0)
+ return NULL;
+ char out[2560];
+ if (location[0] == '/') {
+ if (u.port == 443)
+ snprintf(out, sizeof out, "https://%s%s", u.host, location);
+ else
+ snprintf(out, sizeof out, "https://%s:%d%s", u.host, u.port,
+ location);
+ return xstrdup(out);
+ }
+ const char *last = strrchr(u.path, '/');
+ size_t dir = last ? (size_t)(last - u.path + 1) : 0;
+ char path[2048];
+ if (dir >= sizeof path) {
+ set_err(err, "redirect path is too long");
+ return NULL;
+ }
+ memcpy(path, u.path, dir);
+ snprintf(path + dir, sizeof path - dir, "%s", location);
+ if (u.port == 443)
+ snprintf(out, sizeof out, "https://%s%s", u.host, path);
+ else
+ snprintf(out, sizeof out, "https://%s:%d%s", u.host, u.port, path);
+ return xstrdup(out);
+}
+
+int tax_table_https_get(const char *url, struct buf *out, char **err)
+{
+ char *current = xstrdup(url);
+ for (int hop = 0; hop <= TT_MAX_REDIRECTS; hop++) {
+ struct tt_url u;
+ if (tt_url_parse(current, &u, err) != 0) {
+ free(current);
+ return -1;
+ }
+ struct buf body;
+ buf_init(&body);
+ int status = 0;
+ char *location = NULL;
+ int rc = tt_get_once(&u, &body, &status, &location, err);
+ if (rc != 0) {
+ free(location);
+ buf_free(&body);
+ free(current);
+ return -1;
+ }
+ if (status >= 300 && status < 400 && location && *location) {
+ char *next = tt_resolve(current, location, err);
+ free(location);
+ buf_free(&body);
+ free(current);
+ if (!next)
+ return -1;
+ current = next;
+ continue;
+ }
+ if (status != 200) {
+ set_err(err, "HTTP %d for %.160s", status, current);
+ free(location);
+ buf_free(&body);
+ free(current);
+ return -1;
+ }
+ free(location);
+ free(current);
+ *out = body;
+ return 0;
+ }
+ set_err(err, "too many redirects for %.160s", url);
+ free(current);
+ return -1;
+}
+
+/* ------------------------------------------------------------------ */
+/* Skatteverket page and download */
+/* ------------------------------------------------------------------ */
+
+static char *find_link(const char *html, size_t len, const char *suffix,
+ int index)
+{
+ size_t slen = strlen(suffix);
+ int seen = 0;
+ size_t i = 0;
+ while (i + 6 < len) {
+ if (strncasecmp(html + i, "href=", 5) != 0) {
+ i++;
+ continue;
+ }
+ char quote = html[i + 5];
+ if (quote != '"' && quote != '\'') {
+ i += 5;
+ continue;
+ }
+ size_t start = i + 6;
+ size_t end = start;
+ while (end < len && html[end] != quote)
+ end++;
+ size_t hlen = end - start;
+ if (hlen >= slen &&
+ strncmp(html + end - slen, suffix, slen) == 0) {
+ if (seen == index) {
+ char *href = xmalloc(hlen + 1);
+ memcpy(href, html + start, hlen);
+ href[hlen] = '\0';
+ char *amp = href;
+ while ((amp = strstr(amp, "&amp;")) != NULL) {
+ *amp = '&';
+ memmove(amp + 1, amp + 5, strlen(amp + 5) + 1);
+ }
+ return href;
+ }
+ seen++;
+ }
+ i = end + 1;
+ }
+ return NULL;
+}
+
+int tax_table_fetch_year(int year, unsigned char **data, size_t *len,
+ char **source_url, char **err)
+{
+ *data = NULL;
+ *len = 0;
+ *source_url = NULL;
+ time_t now = time(NULL);
+ struct tm tm;
+ gmtime_r(&now, &tm);
+ int current_year = tm.tm_year + 1900;
+ if (year < 2000 || year > current_year) {
+ set_err(err, "no published table for year %d", year);
+ return -1;
+ }
+
+ struct buf page;
+ buf_init(&page);
+ if (tax_table_https_get(TT_PAGE_URL, &page, err) != 0) {
+ buf_free(&page);
+ return -1;
+ }
+ size_t page_len = page.len;
+ buf_append(&page, "", 1);
+ char *href = find_link((const char *)page.p, page_len, TT_TABLE_FILE,
+ current_year - year);
+ buf_free(&page);
+ if (!href) {
+ set_err(err, "no %s link for %d on Skatteverket's page", TT_TABLE_FILE,
+ year);
+ return -1;
+ }
+ char *abs = tt_resolve(TT_PAGE_URL, href, err);
+ free(href);
+ if (!abs)
+ return -1;
+
+ struct buf file;
+ buf_init(&file);
+ if (tax_table_https_get(abs, &file, err) != 0) {
+ buf_free(&file);
+ free(abs);
+ return -1;
+ }
+ *data = file.p;
+ *len = file.len;
+ *source_url = abs;
+ return 0;
+}