440 lines
13 KiB
C
440 lines
13 KiB
C
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
|
/* ipvarch_db.c - used-networks SQLite backend for IPVarch */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include "vendor/sqlite3.h"
|
|
#include "ipvarch_db.h"
|
|
|
|
/* Schema DDL for the UsedNetworks table: a NULL-terminated array of
|
|
* source lines, generated from schema/used_networks.sql by the
|
|
* Makefile (see schema_sql.c). */
|
|
extern const char *ipvarch_schema_lines[];
|
|
|
|
#define DB_PATH_SZ 640
|
|
|
|
static sqlite3 *g_db = NULL;
|
|
static int g_state = -1; /* -1 = not opened; else DB_* state */
|
|
static char g_path[DB_PATH_SZ] = "";
|
|
static char g_err[256] = "";
|
|
|
|
const char *db_last_error(void) {
|
|
return g_err[0] != '\0' ? g_err : "unknown database error";
|
|
}
|
|
|
|
const char *db_path(void) {
|
|
return g_path;
|
|
}
|
|
|
|
static void record_err(const char *context) {
|
|
const char *msg = (g_db != NULL) ? sqlite3_errmsg(g_db) : NULL;
|
|
if (msg != NULL)
|
|
sqlite3_snprintf((int)sizeof g_err, g_err, "%s: %s", context, msg);
|
|
else
|
|
sqlite3_snprintf((int)sizeof g_err, g_err, "%s", context);
|
|
return;
|
|
}
|
|
|
|
static void resolve_db_path(char *path, int editable) {
|
|
const char *env, *base;
|
|
char dir[512];
|
|
|
|
env = getenv("IPVARCH_DB");
|
|
if (env != NULL && env[0] != '\0') {
|
|
sqlite3_snprintf(DB_PATH_SZ, path, "%s", env);
|
|
return;
|
|
}
|
|
base = getenv("XDG_CONFIG_HOME");
|
|
if (base != NULL && base[0] != '\0') {
|
|
sqlite3_snprintf((int)sizeof dir, dir, "%s/ipvarch", base);
|
|
}
|
|
else {
|
|
base = getenv("HOME");
|
|
if (base == NULL || base[0] == '\0') {
|
|
sqlite3_snprintf(DB_PATH_SZ, path, "networks.db");
|
|
return;
|
|
}
|
|
sqlite3_snprintf((int)sizeof dir, dir, "%s/.config/ipvarch", base);
|
|
}
|
|
if (editable)
|
|
mkdir(dir, 0755); /* silently fine when it exists */
|
|
sqlite3_snprintf(DB_PATH_SZ, path, "%s/networks.db", dir);
|
|
return;
|
|
}
|
|
|
|
static int table_exists(void) {
|
|
sqlite3_stmt *st = NULL;
|
|
int exists = 0;
|
|
|
|
if (sqlite3_prepare_v2(g_db,
|
|
"SELECT count(*) FROM sqlite_master WHERE type='table' "
|
|
"AND name='UsedNetworks'", -1, &st, NULL) != SQLITE_OK) {
|
|
record_err("count tables");
|
|
return -1;
|
|
}
|
|
if (sqlite3_step(st) == SQLITE_ROW && sqlite3_column_int64(st, 0) > 0)
|
|
exists = 1;
|
|
sqlite3_finalize(st);
|
|
return exists;
|
|
}
|
|
|
|
/* Release the SQLite connection (registered via atexit in db_do_open;
|
|
* runs on both normal returns and die()/exit() paths so sanitizers
|
|
* see a clean shutdown). */
|
|
void db_close(void) {
|
|
if (g_db != NULL) {
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
}
|
|
g_state = -1;
|
|
sqlite3_shutdown(); /* free SQLite's one-time globals */
|
|
}
|
|
|
|
static int db_do_open(int editable) {
|
|
static int close_registered = 0;
|
|
int flags, exists;
|
|
|
|
if (g_state != -1)
|
|
return g_state; /* already opened once */
|
|
|
|
resolve_db_path(g_path, editable);
|
|
flags = editable ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)
|
|
: SQLITE_OPEN_READONLY;
|
|
|
|
if (sqlite3_open_v2(g_path, &g_db, flags, NULL) != SQLITE_OK) {
|
|
record_err("open database");
|
|
if (g_db != NULL)
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
g_state = editable ? DB_IO : DB_EMPTY;
|
|
return g_state;
|
|
}
|
|
|
|
if (editable) {
|
|
static char ddl[4096]; /* joined lines of the DDL */
|
|
int i;
|
|
|
|
ddl[0] = '\0';
|
|
for (i = 0; ipvarch_schema_lines[i] != NULL; i++) {
|
|
if (strlen(ddl) + strlen(ipvarch_schema_lines[i]) >= sizeof ddl) {
|
|
record_err("schema DDL exceeds buffer");
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
g_state = DB_IO;
|
|
return g_state;
|
|
}
|
|
strcat(ddl, ipvarch_schema_lines[i]);
|
|
}
|
|
if (sqlite3_exec(g_db, ddl, NULL, NULL, NULL) != SQLITE_OK) {
|
|
record_err("create table");
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
g_state = DB_IO;
|
|
return g_state;
|
|
}
|
|
}
|
|
|
|
if (!close_registered) {
|
|
atexit(db_close);
|
|
close_registered = 1;
|
|
}
|
|
|
|
exists = table_exists();
|
|
if (exists < 0) {
|
|
sqlite3_close(g_db);
|
|
g_db = NULL;
|
|
g_state = editable ? DB_IO : DB_EMPTY;
|
|
return g_state;
|
|
}
|
|
g_state = exists ? DB_OK : DB_EMPTY;
|
|
return g_state;
|
|
}
|
|
|
|
int db_init(int editable) {
|
|
return db_do_open(editable);
|
|
}
|
|
|
|
/* 32-bit mask math (host order). */
|
|
static unsigned long mask_of(int prefix) {
|
|
if (prefix >= 32)
|
|
return 0xFFFFFFFFUL;
|
|
if (prefix <= 0)
|
|
return 0UL;
|
|
return 0xFFFFFFFFUL << (32 - prefix);
|
|
}
|
|
|
|
static unsigned long range_end(unsigned long net, int prefix) {
|
|
return net | (0xFFFFFFFFUL ^ mask_of(prefix));
|
|
}
|
|
|
|
static int interval_overlaps(unsigned long s1, unsigned long e1,
|
|
unsigned long s2, unsigned long e2) {
|
|
return (s2 <= e1 && s1 <= e2) ? 1 : 0;
|
|
}
|
|
|
|
/* Count registered networks intersecting [start,end]. Optional out
|
|
* params (may both be NULL) receive the first match and how many
|
|
* further matches exist. Returns DB_OK or DB_IO. */
|
|
static int scan_used(unsigned long start, unsigned long end,
|
|
char *hit_cidr, int hit_cidr_sz,
|
|
char *hit_label, int hit_label_sz,
|
|
int *out_conflicts, int *out_extra) {
|
|
sqlite3_stmt *st = NULL;
|
|
unsigned long hstart, hend;
|
|
const unsigned char *text;
|
|
int count = 0, extra = 0, rc;
|
|
|
|
if (out_conflicts != NULL)
|
|
*out_conflicts = 0;
|
|
if (out_extra != NULL)
|
|
*out_extra = 0;
|
|
|
|
if (g_state == -1) {
|
|
g_state = db_do_open(0);
|
|
if (g_state != DB_OK) {
|
|
return (g_state == DB_EMPTY) ? DB_OK : DB_IO;
|
|
}
|
|
}
|
|
if (g_state == DB_EMPTY)
|
|
return DB_OK; /* nothing registered: range is free */
|
|
|
|
if (sqlite3_prepare_v2(g_db,
|
|
"SELECT cidr,label,start_hi,start_lo,end_hi,end_lo "
|
|
"FROM UsedNetworks ORDER BY cidr", -1, &st, NULL) != SQLITE_OK) {
|
|
record_err("prepare UsedNetworks scan");
|
|
return DB_IO;
|
|
}
|
|
|
|
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
|
|
hstart = ((unsigned long)sqlite3_column_int64(st, 2)) << 16 |
|
|
((unsigned long)sqlite3_column_int64(st, 3));
|
|
hend = ((unsigned long)sqlite3_column_int64(st, 4)) << 16 |
|
|
((unsigned long)sqlite3_column_int64(st, 5));
|
|
if (interval_overlaps(start, end, hstart, hend)) {
|
|
if (count == 0 && hit_cidr != NULL) {
|
|
text = sqlite3_column_text(st, 0);
|
|
sqlite3_snprintf(hit_cidr_sz, hit_cidr, "%s",
|
|
text ? (const char *)text : "");
|
|
}
|
|
if (count == 0 && hit_label != NULL) {
|
|
text = sqlite3_column_text(st, 1);
|
|
sqlite3_snprintf(hit_label_sz, hit_label, "%s",
|
|
text ? (const char *)text : "");
|
|
}
|
|
if (count > 0)
|
|
extra++;
|
|
count++;
|
|
}
|
|
}
|
|
if (rc != SQLITE_DONE) {
|
|
record_err("step UsedNetworks scan");
|
|
sqlite3_finalize(st);
|
|
return DB_IO;
|
|
}
|
|
sqlite3_finalize(st);
|
|
if (out_conflicts != NULL)
|
|
*out_conflicts = count;
|
|
if (out_extra != NULL)
|
|
*out_extra = extra;
|
|
return DB_OK;
|
|
}
|
|
|
|
int db_add_network(unsigned long net, int prefix, const char *cidr,
|
|
const char *label) {
|
|
sqlite3_stmt *st = NULL;
|
|
unsigned long start, end;
|
|
int rc;
|
|
|
|
if (g_state != DB_OK) {
|
|
g_state = db_do_open(1);
|
|
if (g_state != DB_OK)
|
|
return DB_IO;
|
|
}
|
|
start = net;
|
|
end = range_end(net, prefix);
|
|
|
|
if (sqlite3_prepare_v2(g_db,
|
|
"INSERT INTO UsedNetworks"
|
|
" (cidr,prefix,start_hi,start_lo,end_hi,end_lo,label)"
|
|
" VALUES (?1,?2,?3,?4,?5,?6,?7)", -1, &st, NULL) != SQLITE_OK) {
|
|
record_err("prepare insert");
|
|
return DB_IO;
|
|
}
|
|
sqlite3_bind_text(st, 1, cidr, -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_int64(st, 2, (sqlite3_int64)prefix);
|
|
sqlite3_bind_int64(st, 3, (sqlite3_int64)((start >> 16) & 0xFFFF));
|
|
sqlite3_bind_int64(st, 4, (sqlite3_int64)(start & 0xFFFF));
|
|
sqlite3_bind_int64(st, 5, (sqlite3_int64)((end >> 16) & 0xFFFF));
|
|
sqlite3_bind_int64(st, 6, (sqlite3_int64)(end & 0xFFFF));
|
|
sqlite3_bind_text(st, 7, label, -1, SQLITE_TRANSIENT);
|
|
|
|
rc = sqlite3_step(st);
|
|
sqlite3_finalize(st);
|
|
if (rc == SQLITE_DONE)
|
|
return DB_OK;
|
|
if (rc == SQLITE_CONSTRAINT)
|
|
return DB_DUPLICATE;
|
|
record_err("insert");
|
|
return DB_IO;
|
|
}
|
|
|
|
int db_get_label(const char *cidr, char *buf, int bufsz) {
|
|
sqlite3_stmt *st = NULL;
|
|
const unsigned char *text;
|
|
int rc;
|
|
|
|
if (buf != NULL && bufsz > 0)
|
|
buf[0] = '\0';
|
|
|
|
if (g_state != DB_OK) {
|
|
g_state = db_do_open(1);
|
|
if (g_state != DB_OK)
|
|
return DB_IO;
|
|
}
|
|
if (sqlite3_prepare_v2(g_db,
|
|
"SELECT label FROM UsedNetworks WHERE cidr = ?1",
|
|
-1, &st, NULL) != SQLITE_OK) {
|
|
record_err("prepare label select");
|
|
return DB_IO;
|
|
}
|
|
sqlite3_bind_text(st, 1, cidr, -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(st);
|
|
if (rc == SQLITE_ROW) {
|
|
text = sqlite3_column_text(st, 0);
|
|
sqlite3_snprintf(bufsz, buf, "%s",
|
|
text ? (const char *)text : "");
|
|
sqlite3_finalize(st);
|
|
return DB_OK;
|
|
}
|
|
sqlite3_finalize(st);
|
|
if (rc == SQLITE_DONE)
|
|
return DB_NOT_FOUND;
|
|
record_err("step label select");
|
|
return DB_IO;
|
|
}
|
|
|
|
int db_forget_network(const char *cidr, char *label_out, int label_out_sz) {
|
|
sqlite3_stmt *st = NULL;
|
|
int rc;
|
|
|
|
if (g_state != DB_OK) {
|
|
g_state = db_do_open(1);
|
|
if (g_state != DB_OK)
|
|
return DB_IO;
|
|
}
|
|
|
|
if (label_out != NULL && label_out_sz > 0)
|
|
label_out[0] = '\0';
|
|
rc = db_get_label(cidr, label_out, label_out_sz);
|
|
if (rc != DB_OK)
|
|
return rc;
|
|
|
|
if (sqlite3_prepare_v2(g_db,
|
|
"DELETE FROM UsedNetworks WHERE cidr = ?1",
|
|
-1, &st, NULL) != SQLITE_OK) {
|
|
record_err("prepare delete");
|
|
return DB_IO;
|
|
}
|
|
sqlite3_bind_text(st, 1, cidr, -1, SQLITE_TRANSIENT);
|
|
rc = sqlite3_step(st);
|
|
sqlite3_finalize(st);
|
|
if (rc != SQLITE_DONE || sqlite3_changes(g_db) < 1) {
|
|
record_err("delete");
|
|
return DB_IO;
|
|
}
|
|
return DB_OK;
|
|
}
|
|
|
|
int db_count(int *out) {
|
|
sqlite3_stmt *st = NULL;
|
|
int n = 0;
|
|
|
|
*out = 0;
|
|
if (g_state != DB_OK) {
|
|
g_state = db_do_open(0);
|
|
if (g_state == DB_EMPTY)
|
|
return DB_OK; /* no table yet: count = 0 */
|
|
if (g_state != DB_OK)
|
|
return DB_IO;
|
|
}
|
|
if (g_state == DB_EMPTY)
|
|
return DB_OK; /* count = 0 */
|
|
|
|
if (sqlite3_prepare_v2(g_db, "SELECT count(*) FROM UsedNetworks",
|
|
-1, &st, NULL) != SQLITE_OK) {
|
|
record_err("prepare count");
|
|
return DB_IO;
|
|
}
|
|
if (sqlite3_step(st) == SQLITE_ROW)
|
|
n = (int)sqlite3_column_int64(st, 0);
|
|
sqlite3_finalize(st);
|
|
*out = n;
|
|
return DB_OK;
|
|
}
|
|
|
|
/* 1 = intersecting network registered, 0 = free, DB_IO = error. */
|
|
int db_overlaps_any(unsigned long start, unsigned long end) {
|
|
int conflicts = 0;
|
|
|
|
if (scan_used(start, end, NULL, 0, NULL, 0, &conflicts, NULL) != DB_OK)
|
|
return DB_IO;
|
|
return conflicts > 0 ? 1 : 0;
|
|
}
|
|
|
|
int db_used_by_plan(unsigned long start, unsigned long end,
|
|
char *buf, int bufsz) {
|
|
char cidr[32], label[112];
|
|
int found = 0, extra = 0, len;
|
|
|
|
if (buf != NULL && bufsz > 0)
|
|
buf[0] = '\0';
|
|
if (scan_used(start, end, cidr, (int)sizeof cidr,
|
|
label, (int)sizeof label, &found, &extra) != DB_OK)
|
|
return DB_IO;
|
|
if (found == 0)
|
|
return 0; /* clean range */
|
|
|
|
sqlite3_snprintf(bufsz, buf, "%s (\"%.64s\")", cidr, label);
|
|
if (extra > 0) {
|
|
len = (int)strlen(buf);
|
|
sqlite3_snprintf(bufsz - len, buf + len, " +%d more", extra);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int db_list_networks(db_list_fn cb) {
|
|
sqlite3_stmt *st = NULL;
|
|
const unsigned char *c, *l, *t;
|
|
int rc;
|
|
|
|
if (g_state == -1) {
|
|
g_state = db_do_open(0);
|
|
if (g_state != DB_OK)
|
|
return DB_IO;
|
|
}
|
|
if (g_state == DB_EMPTY)
|
|
return DB_OK; /* callback never fires */
|
|
|
|
if (sqlite3_prepare_v2(g_db,
|
|
"SELECT cidr,label,created_at FROM UsedNetworks ORDER BY cidr",
|
|
-1, &st, NULL) != SQLITE_OK) {
|
|
record_err("prepare list");
|
|
return DB_IO;
|
|
}
|
|
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
|
|
c = sqlite3_column_text(st, 0);
|
|
l = sqlite3_column_text(st, 1);
|
|
t = sqlite3_column_text(st, 2);
|
|
cb(c ? (const char *)c : "",
|
|
l ? (const char *)l : "",
|
|
t ? (const char *)t : "");
|
|
}
|
|
if (rc != SQLITE_DONE) {
|
|
record_err("step list");
|
|
sqlite3_finalize(st);
|
|
return DB_IO;
|
|
}
|
|
sqlite3_finalize(st);
|
|
return DB_OK;
|
|
}
|