#include "tax_table.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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, "&")) != 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; }