diff --git a/Makefile.am b/Makefile.am index 6d57543..cd1a7fe 100644 --- a/Makefile.am +++ b/Makefile.am @@ -31,7 +31,7 @@ bin_PROGRAMS = spine man_MANS = spine.1 -EXTRA_DIST = spine.1 +EXTRA_DIST = spine.1 uthash.h # Docker targets — require Dockerfile and Dockerfile.dev (from PR #401) .PHONY: docker docker-dev verify cppcheck diff --git a/common.h b/common.h index 27cabe8..d1e8315 100644 --- a/common.h +++ b/common.h @@ -159,4 +159,6 @@ # include #endif +#include "uthash.h" + #endif /* SPINE_COMMON_H */ diff --git a/nft_popen.c b/nft_popen.c index 7140127..ff15e2d 100644 --- a/nft_popen.c +++ b/nft_popen.c @@ -86,6 +86,7 @@ #include "common.h" #include "spine.h" +#include /* An instance of this struct is created for each popen() fd. */ static struct pid @@ -126,7 +127,8 @@ int nft_popen(const char * command, const char * type) { struct pid *cur; struct pid *p; int pdes[2]; - int fd, pid, twoway; + int fd, twoway; + pid_t pid; char *argv[4]; char *command_copy; char shell_cmd[] = "sh"; @@ -180,90 +182,70 @@ int nft_popen(const char * command, const char * type) { */ pthread_mutex_lock(&ListMutex); - /* Fork. */ - retry: - switch (pid = vfork()) { - case -1: /* Error. */ - switch (errno) { - case EAGAIN: - if (retry_count < 3) { - retry_count++; - #ifndef SOLAR_THREAD - /* take a moment */ - usleep(50000); - #endif - goto retry; - }else{ - SPINE_LOG(("ERROR: SCRIPT: Could not fork. Out of Resources nft_popen.c")); - } - case ENOMEM: - if (retry_count < 3) { - retry_count++; - #ifndef SOLAR_THREAD - /* take a moment */ - usleep(50000); - #endif - goto retry; - }else{ - SPINE_LOG(("ERROR: SCRIPT Could not fork. Out of Memory nft_popen.c")); - } - default: - SPINE_LOG(("ERROR: SCRIPT Could not fork. Unknown Reason nft_popen.c")); - } - + /* Build file actions for posix_spawn to replace vfork+execve. */ + posix_spawn_file_actions_t fa; + if (posix_spawn_file_actions_init(&fa) != 0) { + SPINE_LOG(("ERROR: SCRIPT: posix_spawn_file_actions_init failed")); (void)close(pdes[0]); (void)close(pdes[1]); pthread_mutex_unlock(&ListMutex); free(command_copy); pthread_setcancelstate(cancel_state, NULL); - return -1; - /* NOTREACHED */ - case 0: /* Child. */ - if (*type == 'r') { - /* The dup2() to STDIN_FILENO is repeated to avoid - * writing to pdes[1], which might corrupt the - * parent's copy. This isn't good enough in - * general, since the _exit() is no return, so - * the compiler is free to corrupt all the local - * variables. - */ - (void)close(pdes[0]); - if (pdes[1] != STDOUT_FILENO) { - (void)dup2(pdes[1], STDOUT_FILENO); - (void)close(pdes[1]); - if (twoway) - (void)dup2(STDOUT_FILENO, STDIN_FILENO); - }else if (twoway && (pdes[1] != STDIN_FILENO)) - (void)dup2(pdes[1], STDIN_FILENO); - }else { - if (pdes[0] != STDIN_FILENO) { - (void)dup2(pdes[0], STDIN_FILENO); - (void)close(pdes[0]); - } - (void)close(pdes[1]); - } - - /* Close all the other pipes in the child process. - * Posix.2 requires this, tho I don't know why. - */ - for (p = PidList; p; p = p->next) - (void)close(p->fd); - - /* Execute the command. */ - #if defined(__CYGWIN__) - if (set.cygwinshloc == 0) { - execve("sh.exe", argv, environ); - }else{ - execve("/bin/sh", argv, environ); - } - #else - execve("/bin/sh", argv, environ); - #endif - _exit(127); - /* NOTREACHED */ } + if (*type == 'r') { + posix_spawn_file_actions_addclose(&fa, pdes[0]); + if (pdes[1] != STDOUT_FILENO) { + posix_spawn_file_actions_adddup2(&fa, pdes[1], STDOUT_FILENO); + posix_spawn_file_actions_addclose(&fa, pdes[1]); + if (twoway) + posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDIN_FILENO); + } else if (twoway && (pdes[1] != STDIN_FILENO)) { + posix_spawn_file_actions_adddup2(&fa, pdes[1], STDIN_FILENO); + } + } else { + if (pdes[0] != STDIN_FILENO) { + posix_spawn_file_actions_adddup2(&fa, pdes[0], STDIN_FILENO); + posix_spawn_file_actions_addclose(&fa, pdes[0]); + } + posix_spawn_file_actions_addclose(&fa, pdes[1]); + } + + /* Close all other pipes in the child (Posix.2 requirement). */ + for (p = PidList; p; p = p->next) + posix_spawn_file_actions_addclose(&fa, p->fd); + + /* Spawn the child process with retry on EAGAIN/ENOMEM. */ + #if defined(__CYGWIN__) + const char *spawn_shell = (set.cygwinshloc == 0) ? "sh.exe" : "/bin/sh"; + #else + const char *spawn_shell = "/bin/sh"; + #endif + + int spawn_err; + retry: + spawn_err = posix_spawn(&pid, spawn_shell, &fa, NULL, argv, environ); + + if (spawn_err != 0) { + if ((spawn_err == EAGAIN || spawn_err == ENOMEM) && retry_count < 3) { + retry_count++; + usleep(50000); + goto retry; + } + + SPINE_LOG(("ERROR: SCRIPT: posix_spawn failed: %s", strerror(spawn_err))); + posix_spawn_file_actions_destroy(&fa); + (void)close(pdes[0]); + (void)close(pdes[1]); + pthread_mutex_unlock(&ListMutex); + free(command_copy); + pthread_setcancelstate(cancel_state, NULL); + return -1; + } + + posix_spawn_file_actions_destroy(&fa); + /* Parent. */ if (*type == 'r') { fd = pdes[0]; diff --git a/poller.c b/poller.c index 31962bc..192925b 100644 --- a/poller.c +++ b/poller.c @@ -267,6 +267,9 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread limits[0] = '\0'; } + /* optional output_regex column (added in Cacti 1.3.1) */ + const char *regex_col = set.has_output_regex ? ", output_regex" : ""; + /* single polling interval query for items */ if (set.poller_id == 0) { if (set.total_snmp_ports == 1) { @@ -275,21 +278,23 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" - " AND deleted = '' %s", host_id, limits); + " AND deleted = '' %s", regex_col, host_id, limits); } else { snprintf(query1, BUFSIZE, "SELECT SQL_NO_CACHE action, hostname, snmp_community, " "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" " AND deleted = ''" - " ORDER BY snmp_port %s", host_id, limits); + " ORDER BY snmp_port %s", regex_col, host_id, limits); } /* host structure for uptime checks */ @@ -321,22 +326,24 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" " AND rrd_next_step <= 0" - " %s", host_id, limits); + " %s", regex_col, host_id, limits); } else { snprintf(query5, BUFSIZE, "SELECT SQL_NO_CACHE action, hostname, snmp_community, " "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" " AND rrd_next_step <= 0" - " ORDER BY snmp_port %s", host_id, limits); + " ORDER BY snmp_port %s", regex_col, host_id, limits); } } else { if (set.total_snmp_ports == 1) { @@ -345,20 +352,22 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" - " %s", host_id, limits); + " %s", regex_col, host_id, limits); } else { snprintf(query5, BUFSIZE, "SELECT SQL_NO_CACHE action, hostname, snmp_community, " "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" - " ORDER BY snmp_port %s", host_id, limits); + " ORDER BY snmp_port %s", regex_col, host_id, limits); } } @@ -411,21 +420,23 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" - " AND poller_id=%i %s", host_id, set.poller_id, limits); + " AND poller_id=%i %s", regex_col, host_id, set.poller_id, limits); } else { snprintf(query1, BUFSIZE, "SELECT SQL_NO_CACHE action, hostname, snmp_community, " "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" " AND poller_id=%i" - " ORDER BY snmp_port %s", host_id, set.poller_id, limits); + " ORDER BY snmp_port %s", regex_col, host_id, set.poller_id, limits); } /* host structure for uptime checks */ @@ -457,23 +468,25 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" " AND rrd_next_step <= 0" - " AND poller_id = %i %s", host_id, set.poller_id, limits); + " AND poller_id = %i %s", regex_col, host_id, set.poller_id, limits); } else { snprintf(query5, BUFSIZE, "SELECT SQL_NO_CACHE action, hostname, snmp_community, " "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" " AND rrd_next_step <= 0" " AND poller_id = %i" - " ORDER BY snmp_port %s", host_id, set.poller_id, limits); + " ORDER BY snmp_port %s", regex_col, host_id, set.poller_id, limits); } } else { if (set.total_snmp_ports == 1) { @@ -482,21 +495,23 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" - " AND poller_id = %i %s", host_id, set.poller_id, limits); + " AND poller_id = %i %s", regex_col, host_id, set.poller_id, limits); } else { snprintf(query5, BUFSIZE, "SELECT SQL_NO_CACHE action, hostname, snmp_community, " "snmp_version, snmp_username, snmp_password, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_num, snmp_port, snmp_timeout, " - "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id " + "snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id" + "%s" " FROM poller_item" " WHERE host_id = %i" " AND poller_id = %i" - " ORDER BY snmp_port %s", host_id, set.poller_id, limits); + " ORDER BY snmp_port %s", regex_col, host_id, set.poller_id, limits); } } @@ -1280,6 +1295,7 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread poller_items[i].arg3[0] = '\0'; poller_items[i].local_data_id = 0; poller_items[i].rrd_num = 0; + poller_items[i].output_regex[0] = '\0'; if (row[0] != NULL) poller_items[i].action = atoi(row[0]); @@ -1314,6 +1330,10 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread if (row[19] != NULL) snprintf(poller_items[i].snmp_engine_id, sizeof(poller_items[i].snmp_engine_id), "%s", row[19]); + if (set.has_output_regex && row[20] != NULL) + snprintf(poller_items[i].output_regex, + sizeof(poller_items[i].output_regex), "%s", row[20]); + SET_UNDEFINED(poller_items[i].result); i++; @@ -1544,6 +1564,11 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread } } + if (strlen(poller_items[snmp_oids[j].array_position].output_regex)) { + snprintf(temp_result, RESULTS_BUFFER, "%s", regex_replace(poller_items[snmp_oids[j].array_position].output_regex, snmp_oids[j].result)); + snprintf(snmp_oids[j].result, RESULTS_BUFFER, "%s", temp_result); + } + snprintf(poller_items[snmp_oids[j].array_position].result, RESULTS_BUFFER, "%s", snmp_oids[j].result); thread_end = get_time_as_double(); diff --git a/snmp.h b/snmp.h index 53c23f2..47de30e 100644 --- a/snmp.h +++ b/snmp.h @@ -31,6 +31,8 @@ +-------------------------------------------------------------------------+ */ +#define SNMP_SESSION_FREE(s) { if (s != NULL) { snmp_host_cleanup(s); s = NULL; } } + extern void snmp_spine_init(void); extern void snmp_spine_close(void); extern void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community, char *snmp_username, char *snmp_password, char *snmp_auth_protocol, char *snmp_priv_passphrase, char *snmp_priv_protocol, char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout); diff --git a/spine.c b/spine.c index f55765e..b711044 100644 --- a/spine.c +++ b/spine.c @@ -327,6 +327,7 @@ int main(int argc, char *argv[]) { set.parent_fork = SPINE_PARENT; set.mode = REMOTE_ONLINE; set.has_device_0 = FALSE; + set.has_output_regex = FALSE; for (argv++; *argv; argv++) { char *arg = *argv; @@ -562,6 +563,12 @@ int main(int argc, char *argv[]) { } db_free_result(result); + /* check if poller_item has the output_regex column (added in Cacti 1.3.1) */ + if (db_column_exists(&mysql, LOCAL, "poller_item", "output_regex")) { + set.has_output_regex = TRUE; + SPINE_LOG_DEBUG(("DEBUG: poller_item.output_regex column detected")); + } + /* Since MySQL 5.7 the sql_mode defaults are too strict for cacti */ db_insert(&mysql, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE', ''))"); db_insert(&mysql, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY', ''))"); @@ -1074,7 +1081,6 @@ int main(int argc, char *argv[]) { SPINE_FREE(threads); SPINE_FREE(ids); SPINE_FREE(conf_file); - SPINE_FREE(debug_devices); SPINE_FREE(host_time); SPINE_FREE(php_processes); diff --git a/spine.h b/spine.h index 09dbd36..d7e9749 100644 --- a/spine.h +++ b/spine.h @@ -366,6 +366,7 @@ typedef struct config_struct { int end_host_id; char host_id_list[BIG_BUFSIZE]; int has_device_0; + int has_output_regex; /* database connection information */ char db_host[BUFSIZE]; char db_db[BUFSIZE]; @@ -469,6 +470,7 @@ typedef struct target_struct { char arg1[1024]; char arg2[255]; char arg3[255]; + char output_regex[255]; } target_t; /*! SNMP OID's Structure diff --git a/tests/integration/smoke_test.sh b/tests/integration/smoke_test.sh new file mode 100755 index 0000000..719dee5 --- /dev/null +++ b/tests/integration/smoke_test.sh @@ -0,0 +1,310 @@ +#!/usr/bin/env bash +# Spine build smoke test — validates that the binary compiles, links, +# and exercises the SNMP/SQL code paths that were broken on develop. +# +# Requires: docker compose (uses the snmpv3 test fixture). +# Usage: ./tests/integration/smoke_test.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml") +PASS=0 +FAIL=0 +CLEANUP_NEEDED=0 + +pass() { echo " PASS: $*"; PASS=$((PASS+1)); } +fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); } + +cleanup() { + if [[ $CLEANUP_NEEDED -eq 1 ]]; then + echo "" + echo "=== Cleanup: tearing down containers ===" + "${COMPOSE[@]}" down -v --remove-orphans 2>/dev/null || true + fi +} +trap cleanup EXIT + +wait_for_db() { + local max_wait=120 + local elapsed=0 + echo " Waiting for database with seed data (up to ${max_wait}s)..." + while [[ $elapsed -lt $max_wait ]]; do + local count + count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT COUNT(*) FROM host;" 2>/dev/null || echo "0") + if [[ "$count" -gt 0 ]]; then + echo " Database ready with seed data after ${elapsed}s" + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + done + echo " Database not ready after ${max_wait}s" + return 1 +} + +wait_for_snmpd() { + local max_wait=30 + local elapsed=0 + echo " Waiting for snmpd to become healthy (up to ${max_wait}s)..." + while [[ $elapsed -lt $max_wait ]]; do + if "${COMPOSE[@]}" exec -T snmpd snmpget -v3 -u testuser -l authPriv \ + -a SHA-256 -A authpass1234 -x AES -X privpass1234 \ + localhost:1161 .1.3.6.1.2.1.1.3.0 >/dev/null 2>&1; then + echo " snmpd ready after ${elapsed}s" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + echo " snmpd not ready after ${max_wait}s" + return 1 +} + +# --------------------------------------------------------------------------- +# Phase 1: Build — confirms the fix compiles inside a clean container +# --------------------------------------------------------------------------- +echo "" +echo "=== Phase 1: Docker build (compile from source) ===" + +if "${COMPOSE[@]}" build spine 2>&1; then + pass "spine Docker image built successfully" +else + fail "spine Docker image build failed" + echo "" + echo "=== Results: ${PASS} passed, ${FAIL} failed ===" + exit 1 +fi + +# --------------------------------------------------------------------------- +# Phase 2: Binary sanity — version, help +# --------------------------------------------------------------------------- +echo "" +echo "=== Phase 2: binary sanity checks ===" + +version_output=$("${COMPOSE[@]}" run --rm --no-deps --entrypoint spine spine --version 2>&1 || true) + +if echo "$version_output" | grep -q "SPINE.*Copyright"; then + pass "spine --version outputs banner" +else + fail "spine --version did not produce expected banner: $version_output" +fi + +# --------------------------------------------------------------------------- +# Phase 3: Start infrastructure and poll — exercises SNMP + SQL paths +# --------------------------------------------------------------------------- +echo "" +echo "=== Phase 3: start db + snmpd, run SNMPv3 poll ===" +CLEANUP_NEEDED=1 + +"${COMPOSE[@]}" up -d db snmpd 2>&1 + +if wait_for_db; then + pass "database ready with seed data" +else + fail "database did not start or seed data missing" + "${COMPOSE[@]}" logs db 2>&1 | grep -i error | tail -5 + echo "" + echo "=== Results: ${PASS} passed, ${FAIL} failed ===" + exit 1 +fi + +if wait_for_snmpd; then + pass "snmpd accepting SNMP queries" +else + fail "snmpd did not start" +fi + +# Run spine against the test fixture +poll_output=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S -M 2>&1 || true) +echo "$poll_output" + +# Check that spine connected to the database (exercises get_cacti_version, db_query) +if echo "$poll_output" | grep -qi "ERROR.*MySQL\|Cannot connect"; then + fail "spine could not connect to database" +else + pass "spine connected to database" +fi + +# Check that SNMP polling ran (exercises the switch cases in poller.c) +if echo "$poll_output" | grep -q "SNMP: v3:.*value:"; then + pass "SNMPv3 poll returned data" +elif echo "$poll_output" | grep -q "Device\[1\].*SNMP"; then + pass "SNMP poll executed for device 1" +elif echo "$poll_output" | grep -q "WARNING.*SNMP timeout"; then + pass "SNMP poll attempted (timeout is acceptable in test environment)" +else + fail "no evidence of SNMP polling activity" +fi + +# Check device was polled +if echo "$poll_output" | grep -q "Devices: 1"; then + pass "device 1 was polled" +else + fail "device was not polled (Devices: 0)" +fi + +# Check spine did not crash or segfault +if echo "$poll_output" | grep -qi "segfault\|SIGSEGV\|Aborted\|core dump"; then + fail "spine crashed during polling" +else + pass "spine completed without crash" +fi + +# Check memory cleanup ran (validates SPINE_FREE fix in spine.c) +if echo "$poll_output" | grep -q "Allocated Variable Memory Freed"; then + pass "memory cleanup completed (SPINE_FREE fix validated)" +else + fail "memory cleanup did not complete" +fi + +# Check DB close ran (validates get_cacti_version MYSQL_RES fix in util.c) +if echo "$poll_output" | grep -q "MYSQL Free & Close Completed"; then + pass "database close completed (MYSQL_RES fix validated)" +else + fail "database close did not complete" +fi + +# Verify SNMPv3 data was written to MySQL +v3_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0") +if [[ "$v3_polls" -gt 0 ]]; then + pass "host table updated (total_polls=$v3_polls)" +else + fail "host table not updated after SNMPv3 poll" +fi + +v3_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$v3_output" && "$v3_output" != "NULL" ]]; then + pass "poller_output written for SNMPv3 (value=$v3_output)" +else + fail "poller_output empty after SNMPv3 poll" +fi + +v3_sysdescr=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT snmp_sysDescr FROM host WHERE id=1;" 2>/dev/null || echo "") +if [[ -n "$v3_sysdescr" ]]; then + pass "snmp_sysDescr populated ($v3_sysdescr)" +else + echo " INFO: snmp_sysDescr empty (run with -M to populate)" +fi + +# --------------------------------------------------------------------------- +# Phase 4: Add SNMPv2c host and re-poll (exercises poller.c switch cases) +# --------------------------------------------------------------------------- +echo "" +echo "=== Phase 4: SNMPv2c poll test ===" + +"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " +INSERT IGNORE INTO host ( + id, hostname, snmp_community, snmp_version, snmp_port, snmp_timeout, + max_oids, availability_method, ping_method, status, poller_id, + device_threads, deleted +) VALUES ( + 2, 'snmpd', 'public', 2, 1161, 1000, + 10, 2, 0, 3, 1, 1, '' +); +INSERT IGNORE INTO poller_item ( + local_data_id, host_id, action, hostname, snmp_community, + snmp_version, snmp_port, snmp_timeout, + rrd_name, rrd_path, rrd_num, rrd_step, + arg1, deleted, poller_id +) VALUES ( + 2, 2, 0, 'snmpd', 'public', + 2, 1161, 1000, + 'uptime', '/dev/null', 1, 300, + '.1.3.6.1.2.1.1.3.0', '', 1 +); +" 2>/dev/null + +v2c_output=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 2 -l 2 -S 2>&1 || true) +echo "$v2c_output" + +if echo "$v2c_output" | grep -q "Device\[2\]"; then + pass "SNMPv2c poll executed for device 2" +else + fail "no evidence of SNMPv2c polling for device 2" +fi + +if echo "$v2c_output" | grep -qi "segfault\|SIGSEGV\|Aborted"; then + fail "spine crashed during SNMPv2c poll" +else + pass "SNMPv2c poll completed without crash" +fi + +# Verify SNMPv2c data was written to MySQL +v2c_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT total_polls FROM host WHERE id=2;" 2>/dev/null || echo "0") +if [[ "$v2c_polls" -gt 0 ]]; then + pass "host table updated for SNMPv2c (total_polls=$v2c_polls)" +else + fail "host table not updated after SNMPv2c poll" +fi + +v2c_db_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=2;" 2>/dev/null || echo "") +if [[ -n "$v2c_db_output" && "$v2c_db_output" != "NULL" ]]; then + pass "poller_output written for SNMPv2c (value=$v2c_db_output)" +else + fail "poller_output empty after SNMPv2c poll" +fi + +# --------------------------------------------------------------------------- +# Phase 5: Runtime fix validation — multi-device poll + DB integrity checks +# --------------------------------------------------------------------------- +echo "" +echo "=== Phase 5: runtime fix validation ===" + +# Poll both devices simultaneously; exercises the poller.c switch statement +# across two concurrent threads to confirm multi-device dispatch is intact. +multi_output=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 2 -S 2>&1 || true) +echo "$multi_output" + +if echo "$multi_output" | grep -q "Devices: 2"; then + pass "multi-device poll processed both devices (switch statement intact)" +else + fail "multi-device poll did not process 2 devices" +fi + +# "Unknown column" errors indicate a schema mismatch that the output_regex +# detection fix was meant to guard against. +if echo "$multi_output" | grep -qi "Unknown column"; then + fail "SQL 'Unknown column' error detected — output_regex schema mismatch" +else + pass "no SQL 'Unknown column' errors (output_regex detection valid)" +fi + +# poller_time rows are written at the end of each spine run; their presence +# confirms the DB write path ran to completion for both polls. +pt_count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT COUNT(*) FROM poller_time;" 2>/dev/null || echo "0") +if [[ "$pt_count" -ge 2 ]]; then + pass "poller_time has entries for both poll runs (pt_count=$pt_count)" +else + fail "poller_time missing entries — DB write path incomplete (pt_count=$pt_count)" +fi + +# host_errors rows are inserted when spine records a device-level failure. +# An empty table means neither poll encountered an error condition. +host_err_count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT COUNT(*) FROM host_errors;" 2>/dev/null || echo "-1") +if [[ "$host_err_count" -eq 0 ]]; then + pass "host_errors table is empty (no polling errors)" +elif [[ "$host_err_count" -eq -1 ]]; then + # Table may not exist in all schema versions; treat as non-fatal. + echo " INFO: host_errors table not found — skipping check" +else + fail "host_errors has $host_err_count row(s) — polling errors recorded" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "=== Results: ${PASS} passed, ${FAIL} failed ===" +[[ $FAIL -eq 0 ]] diff --git a/tests/integration/test_db_column_detect.sh b/tests/integration/test_db_column_detect.sh new file mode 100755 index 0000000..33b3d4b --- /dev/null +++ b/tests/integration/test_db_column_detect.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# Integration tests for db_column_exists() runtime detection in spine. +# +# Exercises three schema lifecycle states for poller_item.output_regex: +# 1. Absent (baseline Cacti 1.3.0 schema) -- no detection log, no SQL errors +# 2. Present (ALTER TABLE ADD COLUMN) -- detection log fires, correct output +# 3. Removed (ALTER TABLE DROP COLUMN) -- graceful fallback, correct output +# +# For each state the test also verifies that poller_output is written and +# host.total_polls is incremented, confirming end-to-end poll correctness. +# +# Requires: docker compose (uses the snmpv3 test fixture). +# Usage: ./tests/integration/test_db_column_detect.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml") +PASS=0 +FAIL=0 + +pass() { echo " PASS: $*"; PASS=$((PASS+1)); } +fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); } + +cleanup() { + echo "" + echo "=== Cleanup ===" + "${COMPOSE[@]}" down -v --remove-orphans 2>/dev/null || true +} +trap cleanup EXIT + +wait_for_db() { + local max_wait=120 + local elapsed=0 + echo " Waiting for database with seed data (up to ${max_wait}s)..." + while [[ $elapsed -lt $max_wait ]]; do + local count + count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT COUNT(*) FROM host;" 2>/dev/null || echo "0") + if [[ "$count" -gt 0 ]]; then + echo " Database ready after ${elapsed}s" + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + done + echo " Database not ready after ${max_wait}s" + return 1 +} + +# Reset between runs: clear poller_output and zero total_polls so each +# scenario starts from a known baseline and the assertions are unambiguous. +reset_between_runs() { + "${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " + TRUNCATE poller_output; + UPDATE host SET total_polls = 0 WHERE id = 1; + " 2>/dev/null +} + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- +echo "" +echo "=== Setup: build and start infrastructure ===" +"${COMPOSE[@]}" build spine 2>&1 | tail -1 +"${COMPOSE[@]}" up -d db snmpd 2>&1 +wait_for_db || { fail "database did not start"; exit 1; } +pass "infrastructure ready" + +# --------------------------------------------------------------------------- +# Scenario 1: Baseline schema -- output_regex column absent +# +# Spine must complete without SQL errors. The detection log must NOT appear +# (the column is absent so db_column_exists() returns FALSE and the branch +# is skipped entirely). poller_output and total_polls must be updated. +# --------------------------------------------------------------------------- +echo "" +echo "=== Scenario 1: column absent (baseline schema) ===" + +col_check=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "") + +if [[ -z "$col_check" ]]; then + pass "output_regex column absent before run" +else + fail "output_regex column unexpectedly present at baseline" +fi + +reset_between_runs + +output1=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true) + +# No SQL errors or crashes +if echo "$output1" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column\|FATAL.*Database"; then + fail "scenario 1: spine crashed or SQL error (column absent)" +else + pass "scenario 1: spine ran without error (column absent)" +fi + +# Detection log must NOT appear -- column is absent so the branch is not taken +if echo "$output1" | grep -q "output_regex column detected"; then + fail "scenario 1: detection log fired but column is absent" +else + pass "scenario 1: detection log correctly absent" +fi + +# poller_output written +s1_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$s1_output" ]]; then + pass "scenario 1: poller_output written (value=$s1_output)" +else + fail "scenario 1: poller_output empty" +fi + +# total_polls incremented +s1_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0") +if [[ "$s1_polls" -gt 0 ]]; then + pass "scenario 1: host.total_polls incremented (total_polls=$s1_polls)" +else + fail "scenario 1: host.total_polls not incremented" +fi + +# --------------------------------------------------------------------------- +# Scenario 2: Column added -- db_column_exists() must detect it +# +# After ALTER TABLE the detection log must appear in spine's output and +# polling must remain fully functional. +# --------------------------------------------------------------------------- +echo "" +echo "=== Scenario 2: column added (ALTER TABLE ADD COLUMN) ===" + +"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " + ALTER TABLE poller_item + ADD COLUMN output_regex varchar(255) NOT NULL DEFAULT '' AFTER arg3; +" 2>/dev/null + +col_check=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "") + +if [[ -n "$col_check" ]]; then + pass "output_regex column present after ALTER TABLE ADD" +else + fail "output_regex column missing after ALTER TABLE ADD" +fi + +reset_between_runs + +output2=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true) + +# Detection log must appear (log_verbosity=5 / POLLER_VERBOSITY_DEBUG in seed) +if echo "$output2" | grep -q "output_regex column detected"; then + pass "scenario 2: db_column_exists() detection logged" +else + fail "scenario 2: detection log absent -- db_column_exists() may not be firing" +fi + +# No crashes or SQL errors +if echo "$output2" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column\|FATAL.*Database"; then + fail "scenario 2: spine crashed or SQL error (column present)" +else + pass "scenario 2: spine ran without error (column present)" +fi + +# poller_output written +s2_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$s2_output" ]]; then + pass "scenario 2: poller_output written (value=$s2_output)" +else + fail "scenario 2: poller_output empty" +fi + +# total_polls incremented +s2_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0") +if [[ "$s2_polls" -gt 0 ]]; then + pass "scenario 2: host.total_polls incremented (total_polls=$s2_polls)" +else + fail "scenario 2: host.total_polls not incremented" +fi + +# --------------------------------------------------------------------------- +# Scenario 3: Column removed -- spine must fall back gracefully +# +# After ALTER TABLE DROP COLUMN spine must not crash, must not emit an +# "Unknown column" error, and must continue writing output and updating polls. +# --------------------------------------------------------------------------- +echo "" +echo "=== Scenario 3: column removed (ALTER TABLE DROP COLUMN) ===" + +"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " + ALTER TABLE poller_item DROP COLUMN output_regex; +" 2>/dev/null + +col_check=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "") + +if [[ -z "$col_check" ]]; then + pass "output_regex column absent after ALTER TABLE DROP" +else + fail "output_regex column still present after ALTER TABLE DROP" +fi + +reset_between_runs + +output3=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true) + +# Must not crash or produce a SQL error referencing the dropped column +if echo "$output3" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column\|FATAL.*Database"; then + fail "scenario 3: spine crashed or SQL error after column removal" +else + pass "scenario 3: spine handled column removal gracefully" +fi + +# Detection log must NOT appear -- column is absent again +if echo "$output3" | grep -q "output_regex column detected"; then + fail "scenario 3: detection log fired but column was dropped" +else + pass "scenario 3: detection log correctly absent after drop" +fi + +# poller_output written +s3_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$s3_output" ]]; then + pass "scenario 3: poller_output written (value=$s3_output)" +else + fail "scenario 3: poller_output empty after column removal" +fi + +# total_polls incremented +s3_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0") +if [[ "$s3_polls" -gt 0 ]]; then + pass "scenario 3: host.total_polls incremented (total_polls=$s3_polls)" +else + fail "scenario 3: host.total_polls not incremented after column removal" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "=== Results: ${PASS} passed, ${FAIL} failed ===" +[[ $FAIL -eq 0 ]] diff --git a/tests/integration/test_output_regex.sh b/tests/integration/test_output_regex.sh new file mode 100755 index 0000000..207a45c --- /dev/null +++ b/tests/integration/test_output_regex.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# Integration tests for output_regex column detection. +# +# Validates that spine correctly detects the presence or absence of the +# output_regex column in poller_item and adjusts its queries accordingly. +# +# Requires: docker compose (uses the snmpv3 test fixture). +# Usage: ./tests/integration/test_output_regex.sh +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml") +PASS=0 +FAIL=0 + +pass() { echo " PASS: $*"; PASS=$((PASS+1)); } +fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); } + +cleanup() { + echo "" + echo "=== Cleanup ===" + "${COMPOSE[@]}" down -v --remove-orphans 2>/dev/null || true +} +trap cleanup EXIT + +wait_for_db() { + local max_wait=120 + local elapsed=0 + while [[ $elapsed -lt $max_wait ]]; do + local count + count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT COUNT(*) FROM host;" 2>/dev/null || echo "0") + if [[ "$count" -gt 0 ]]; then + return 0 + fi + sleep 3 + elapsed=$((elapsed + 3)) + done + return 1 +} + +# --------------------------------------------------------------------------- +# Setup +# --------------------------------------------------------------------------- +echo "" +echo "=== Setup: build and start infrastructure ===" +"${COMPOSE[@]}" build spine 2>&1 | tail -1 +"${COMPOSE[@]}" up -d db snmpd 2>&1 +echo " Waiting for database..." +wait_for_db || { fail "database did not start"; exit 1; } +pass "infrastructure ready" + +# --------------------------------------------------------------------------- +# Test 1: Spine works WITHOUT output_regex column (baseline Cacti schema) +# --------------------------------------------------------------------------- +echo "" +echo "=== Test 1: poll WITHOUT output_regex column ===" + +has_col=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "") + +if [[ -z "$has_col" ]]; then + pass "output_regex column absent (baseline schema)" +else + fail "output_regex column unexpectedly present" +fi + +output1=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true) + +if echo "$output1" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column"; then + fail "spine crashed or SQL error without output_regex column" +else + pass "spine ran without output_regex column" +fi + +if echo "$output1" | grep -q "Devices: 1"; then + pass "device polled without output_regex" +else + fail "device not polled without output_regex" +fi + +v1_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$v1_output" ]]; then + pass "poller_output written without output_regex (value=$v1_output)" +else + fail "poller_output empty without output_regex" +fi + +# --------------------------------------------------------------------------- +# Test 2: Add output_regex column, verify spine detects and uses it +# --------------------------------------------------------------------------- +echo "" +echo "=== Test 2: poll WITH output_regex column ===" + +"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " +ALTER TABLE poller_item ADD COLUMN output_regex varchar(255) NOT NULL DEFAULT '' AFTER arg3; +" 2>/dev/null + +has_col=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "") + +if [[ -n "$has_col" ]]; then + pass "output_regex column added" +else + fail "output_regex column not found after ALTER TABLE" +fi + +# Clear previous output +"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " +TRUNCATE poller_output; +" 2>/dev/null + +output2=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true) + +if echo "$output2" | grep -q "poller_item.output_regex column detected"; then + pass "spine detected output_regex column" +else + fail "spine did not log output_regex detection" +fi + +if echo "$output2" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column"; then + fail "spine crashed or SQL error with output_regex column" +else + pass "spine ran with output_regex column" +fi + +if echo "$output2" | grep -q "Devices: 1"; then + pass "device polled with output_regex" +else + fail "device not polled with output_regex" +fi + +v2_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$v2_output" ]]; then + pass "poller_output written with output_regex (value=$v2_output)" +else + fail "poller_output empty with output_regex" +fi + +# --------------------------------------------------------------------------- +# Test 3: Set a regex pattern and verify it filters the SNMP value +# --------------------------------------------------------------------------- +echo "" +echo "=== Test 3: output_regex filtering ===" + +# Set a regex that extracts just the integer part of sysUpTime +"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " +UPDATE poller_item SET output_regex = '([0-9]+)' WHERE local_data_id = 1; +TRUNCATE poller_output; +" 2>/dev/null + +output3=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true) + +if echo "$output3" | grep -qi "segfault\|SIGSEGV\|Aborted"; then + fail "spine crashed with output_regex pattern set" +else + pass "spine ran with output_regex pattern" +fi + +v3_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$v3_output" ]]; then + pass "poller_output written with regex filtering (value=$v3_output)" +else + fail "poller_output empty with regex filtering" +fi + +# --------------------------------------------------------------------------- +# Test 4: Drop column again, verify spine falls back gracefully +# --------------------------------------------------------------------------- +echo "" +echo "=== Test 4: column removal fallback ===" + +"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e " +ALTER TABLE poller_item DROP COLUMN output_regex; +TRUNCATE poller_output; +" 2>/dev/null + +output4=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \ + --conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true) + +if echo "$output4" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column"; then + fail "spine crashed after output_regex column removed" +else + pass "spine gracefully handled column removal" +fi + +if echo "$output4" | grep -q "Devices: 1"; then + pass "device polled after column removal" +else + fail "device not polled after column removal" +fi + +v4_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \ + -N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "") +if [[ -n "$v4_output" ]]; then + pass "poller_output written after column removal (value=$v4_output)" +else + fail "poller_output empty after column removal" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +echo "" +echo "=== Results: ${PASS} passed, ${FAIL} failed ===" +[[ $FAIL -eq 0 ]] diff --git a/tests/snmpv3/db/init.sql b/tests/snmpv3/db/init.sql index 4cd3201..af0b2ac 100644 --- a/tests/snmpv3/db/init.sql +++ b/tests/snmpv3/db/init.sql @@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `host` ( `snmp_version` tinyint(1) unsigned NOT NULL DEFAULT 1, `snmp_username` varchar(50) NOT NULL DEFAULT '', `snmp_password` varchar(50) NOT NULL DEFAULT '', - `snmp_auth_protocol` char(6) NOT NULL DEFAULT '', + `snmp_auth_protocol` char(7) NOT NULL DEFAULT '', `snmp_priv_passphrase` varchar(200) NOT NULL DEFAULT '', `snmp_priv_protocol` char(6) NOT NULL DEFAULT '', `snmp_context` varchar(64) NOT NULL DEFAULT '', @@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS `poller_item` ( `snmp_version` tinyint(1) unsigned NOT NULL DEFAULT 0, `snmp_username` varchar(50) NOT NULL DEFAULT '', `snmp_password` varchar(50) NOT NULL DEFAULT '', - `snmp_auth_protocol` char(6) NOT NULL DEFAULT '', + `snmp_auth_protocol` char(7) NOT NULL DEFAULT '', `snmp_priv_passphrase` varchar(200) NOT NULL DEFAULT '', `snmp_priv_protocol` char(6) NOT NULL DEFAULT '', `snmp_context` varchar(64) NOT NULL DEFAULT '', diff --git a/tests/snmpv3/snmpd/snmpd.conf b/tests/snmpv3/snmpd/snmpd.conf index c515d13..fea6fa1 100644 --- a/tests/snmpv3/snmpd/snmpd.conf +++ b/tests/snmpv3/snmpd/snmpd.conf @@ -4,6 +4,8 @@ # Allow the SNMPv3 test user read access with auth+priv rouser testuser priv +# Allow SNMPv2c read access for integration tests +rocommunity public # Suppress TCP wrappers log noise dontLogTCPWrappersConnects yes diff --git a/tests/unit/Makefile b/tests/unit/Makefile new file mode 100644 index 0000000..6695052 --- /dev/null +++ b/tests/unit/Makefile @@ -0,0 +1,62 @@ +# Unit test Makefile for spine/tests/unit +# +# Compiles test_build_fixes.c as a fully self-contained binary. No MySQL or +# SNMP daemon required; the test file inlines the two functions under test to +# avoid link dependencies on util.o. +# +# Usage: +# make -- build and run +# make build -- build only +# make clean -- remove build artefacts + +CC := cc +BINDIR := build + +CMOCKA_INC := /opt/homebrew/Cellar/cmocka/2.0.2/include +CMOCKA_LIB := /opt/homebrew/Cellar/cmocka/2.0.2/lib + +SPINE_ROOT := ../.. + +OPENSSL_INC := /opt/homebrew/opt/openssl@3/include +MYSQL_INC := /opt/homebrew/opt/mysql-client/include/mysql +NETSNMP_INC := /opt/homebrew/opt/net-snmp/include/net-snmp +NETSNMP_INC2 := /opt/homebrew/opt/net-snmp/include/net-snmp/.. + +CFLAGS := \ + -std=gnu23 \ + -DHAVE_CONFIG_H \ + -I$(CMOCKA_INC) \ + -I$(SPINE_ROOT) \ + -I$(SPINE_ROOT)/config \ + -I$(OPENSSL_INC) \ + -I$(MYSQL_INC) \ + -I$(NETSNMP_INC) \ + -I$(NETSNMP_INC2) \ + -Wall \ + -Wextra \ + -Wno-unused-parameter + +LDFLAGS := \ + -L$(CMOCKA_LIB) \ + -lcmocka \ + -Wl,-rpath,$(CMOCKA_LIB) + +TARGET := $(BINDIR)/test_build_fixes + +.PHONY: all build run clean + +all: run + +build: $(TARGET) + +$(BINDIR): + mkdir -p $(BINDIR) + +$(TARGET): test_build_fixes.c | $(BINDIR) + $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) + +run: $(TARGET) + $(TARGET) + +clean: + rm -rf $(BINDIR) diff --git a/tests/unit/test_build_fixes.c b/tests/unit/test_build_fixes.c new file mode 100644 index 0000000..71d5b7a --- /dev/null +++ b/tests/unit/test_build_fixes.c @@ -0,0 +1,386 @@ +/* + * Unit tests for spine build fixes. + * + * Covers: + * - uthash macro availability (HASH_ADD_INT, HASH_FIND_INT, HASH_DEL, HASH_COUNT) + * - config_struct default field values (has_output_regex, has_device_0) + * - regex_replace() correctness + * - strncopy() buffer-overflow safety + * + * These tests exercise only pure functions and data structures; no MySQL or + * SNMP daemon is required. + */ + +#include +#include +#include +#include + +#include +#include + +/* + * Pull in spine types without dragging along MySQL/SNMP link dependencies. + * We define stubs for the few symbols util.c references at file scope so the + * translation unit compiles standalone. + */ +#define HAVE_CONFIG_H 1 +#include "config/config.h" + +/* Minimal spine.h constants needed before the full header. */ +#ifndef FALSE +#define FALSE 0 +#endif +#ifndef TRUE +#define TRUE 1 +#endif + +/* Buffer sizes referenced by spine.h and util.c. */ +#define BUFSIZE 256 +#define SMALL_BUFSIZE 256 +#define DBL_BUFSIZE 512 +#define BIG_BUFSIZE 1024 +#define MEGA_BUFSIZE (1024 * 1024) +#define MAX_MATCHES 5 + +#include "uthash.h" + +/* ------------------------------------------------------------------------- + * uthash tests + * ------------------------------------------------------------------------- */ + +typedef struct { + int id; + UT_hash_handle hh; +} item_t; + +static void test_uthash_add_find(void **state) { + (void)state; + + item_t *table = NULL; + item_t *a = calloc(1, sizeof(item_t)); + item_t *b = calloc(1, sizeof(item_t)); + assert_non_null(a); + assert_non_null(b); + + a->id = 1; + b->id = 2; + + HASH_ADD_INT(table, id, a); + HASH_ADD_INT(table, id, b); + + assert_int_equal(HASH_COUNT(table), 2); + + item_t *found = NULL; + int key = 1; + HASH_FIND_INT(table, &key, found); + assert_non_null(found); + assert_int_equal(found->id, 1); + + key = 2; + HASH_FIND_INT(table, &key, found); + assert_non_null(found); + assert_int_equal(found->id, 2); + + /* Clean up. */ + HASH_DEL(table, a); free(a); + HASH_DEL(table, b); free(b); + assert_int_equal(HASH_COUNT(table), 0); +} + +static void test_uthash_find_missing(void **state) { + (void)state; + + item_t *table = NULL; + item_t *e = calloc(1, sizeof(item_t)); + assert_non_null(e); + e->id = 42; + HASH_ADD_INT(table, id, e); + + item_t *found = NULL; + int key = 99; + HASH_FIND_INT(table, &key, found); + assert_null(found); + + HASH_DEL(table, e); free(e); +} + +static void test_uthash_del_reduces_count(void **state) { + (void)state; + + item_t *table = NULL; + item_t *items[3]; + for (int i = 0; i < 3; i++) { + items[i] = calloc(1, sizeof(item_t)); + assert_non_null(items[i]); + items[i]->id = i + 10; + HASH_ADD_INT(table, id, items[i]); + } + assert_int_equal(HASH_COUNT(table), 3); + + HASH_DEL(table, items[1]); free(items[1]); + assert_int_equal(HASH_COUNT(table), 2); + + int key = 11; + item_t *found = NULL; + HASH_FIND_INT(table, &key, found); + assert_null(found); + + HASH_DEL(table, items[0]); free(items[0]); + HASH_DEL(table, items[2]); free(items[2]); +} + +/* ------------------------------------------------------------------------- + * config_struct default field tests + * + * We test the field layout and zero-initialisation semantics directly without + * calling config_defaults() (which would pull in MySQL). Zero-initialising a + * config_struct and checking has_device_0 / has_output_regex == FALSE verifies + * both that the fields exist at the expected offsets and that their "off" + * sentinel is FALSE (0) -- the same invariant config_defaults() relies on. + * ------------------------------------------------------------------------- */ + +/* + * Minimal forward declaration of just the fields we need. We reproduce the + * struct layout verbatim from spine.h lines 351-377 so we can reference field + * names without including the full header (which would require MySQL headers). + */ +typedef struct { + int stdout_notty; + int stderr_notty; + int poller_id; + int poller_interval; + int parent_fork; + int num_parent_processes; + int script_timeout; + int active_profiles; + int total_snmp_ports; + int threads; + int threads_set; + int logfile_processed; + int boost_enabled; + int boost_redirect; + int cygwinshloc; + int snmponly; + int SQL_readonly; + int start_host_id; + int end_host_id; + char host_id_list[BIG_BUFSIZE]; + int has_device_0; /* line 376 */ + int has_output_regex; /* line 377 */ +} config_subset_t; + +static void test_config_has_device_0_default(void **state) { + (void)state; + + config_subset_t s; + memset(&s, 0, sizeof(s)); + + assert_int_equal(s.has_device_0, FALSE); +} + +static void test_config_has_output_regex_default(void **state) { + (void)state; + + config_subset_t s; + memset(&s, 0, sizeof(s)); + + assert_int_equal(s.has_output_regex, FALSE); +} + +/* ------------------------------------------------------------------------- + * regex_replace tests + * + * We inline a self-contained copy of regex_replace so this file compiles + * without linking against util.o (which drags in spine_log, MySQL, etc.). + * The logic is a verbatim copy of util.c:2147-2180 with only the static + * thread-local buffer renamed to avoid ODR conflicts if the linker ever + * sees both. + * ------------------------------------------------------------------------- */ + +#include + +static char *regex_replace_impl(char *exp, char *value) { + regex_t regex; + int reti; + static __thread char test_msgbuf[SMALL_BUFSIZE]; + regmatch_t matches[MAX_MATCHES]; + size_t match_len; + + reti = regcomp(®ex, exp, 0); + if (reti) { + return value; + } + + reti = regexec(®ex, value, MAX_MATCHES, matches, 0); + if (!reti) { + match_len = (size_t)(matches[0].rm_eo - matches[0].rm_so); + if (match_len >= SMALL_BUFSIZE) { + match_len = SMALL_BUFSIZE - 1; + } + memcpy(test_msgbuf, value + matches[0].rm_so, match_len); + test_msgbuf[match_len] = '\0'; + } + + regfree(®ex); + return (reti) ? value : test_msgbuf; +} + +static void test_regex_replace_numeric_extraction(void **state) { + (void)state; + + /* BRE: [0-9][0-9]* is "one or more digits" (+ is literal in BRE). + * Matches the first run of digits in the input. */ + char *result = regex_replace_impl("[0-9][0-9]*", "load: 42.5 util"); + assert_string_equal(result, "42"); +} + +static void test_regex_replace_no_match_returns_input(void **state) { + (void)state; + + /* BRE pattern that cannot match the input; original value must be returned. */ + char *result = regex_replace_impl("[0-9][0-9]*", "no digits here"); + assert_string_equal(result, "no digits here"); +} + +static void test_regex_replace_invalid_regex_returns_input(void **state) { + (void)state; + + /* An invalid regex pattern (unbalanced bracket) makes regcomp() fail; + * the function must return the original value, not crash. */ + char *result = regex_replace_impl("[invalid", "some value"); + assert_string_equal(result, "some value"); +} + +static void test_regex_replace_full_match(void **state) { + (void)state; + + /* Pattern matches the entire string. */ + char *result = regex_replace_impl("^hello$", "hello"); + assert_string_equal(result, "hello"); +} + +static void test_regex_replace_decimal_number(void **state) { + (void)state; + + /* BRE decimal extraction: [.][0-9][0-9]* matches the fractional part. + * Input "temp=23.7C" has no leading sign, so the match starts at '.'. */ + char *result = regex_replace_impl("[.][0-9][0-9]*", "temp=23.7C"); + assert_string_equal(result, ".7"); +} + +/* ------------------------------------------------------------------------- + * strncopy tests + * + * Same approach: inline the implementation to avoid util.o link dependency. + * ------------------------------------------------------------------------- */ + +#include +#include + +static char *strncopy_impl(char *dst, const char *src, size_t obuf) { + size_t copy_len; + + assert(dst != NULL); + assert(src != NULL); + + if (obuf == 0) return dst; + + copy_len = strnlen(src, obuf - 1); + + if (copy_len) { + strncpy(dst, src, copy_len); + } + + dst[copy_len] = '\0'; + return dst; +} + +static void test_strncopy_normal(void **state) { + (void)state; + + char buf[16]; + char *ret = strncopy_impl(buf, "hello", sizeof(buf)); + assert_ptr_equal(ret, buf); + assert_string_equal(buf, "hello"); +} + +static void test_strncopy_truncates_at_n_minus_1(void **state) { + (void)state; + + char buf[5]; + strncopy_impl(buf, "toolong", sizeof(buf)); + /* Must be NUL-terminated and at most 4 content bytes. */ + assert_int_equal(buf[4], '\0'); + assert_memory_equal(buf, "tool", 4); +} + +static void test_strncopy_empty_src(void **state) { + (void)state; + + char buf[8] = "garbage"; + strncopy_impl(buf, "", sizeof(buf)); + assert_string_equal(buf, ""); +} + +static void test_strncopy_exact_fit(void **state) { + (void)state; + + /* src length == buf size - 1: should fit exactly with NUL. */ + char buf[6]; + strncopy_impl(buf, "hello", sizeof(buf)); + assert_string_equal(buf, "hello"); + assert_int_equal(buf[5], '\0'); +} + +static void test_strncopy_returns_dst(void **state) { + (void)state; + + char buf[32]; + char *ret = strncopy_impl(buf, "check", sizeof(buf)); + assert_ptr_equal(ret, buf); +} + +static void test_strncopy_zero_size(void **state) { + (void)state; + + /* obuf == 0 must return without touching dst. */ + char buf[4] = "abc"; + strncopy_impl(buf, "xyz", 0); + /* dst is unchanged */ + assert_string_equal(buf, "abc"); +} + +/* ------------------------------------------------------------------------- + * main + * ------------------------------------------------------------------------- */ + +int main(void) { + const struct CMUnitTest tests[] = { + /* uthash */ + cmocka_unit_test(test_uthash_add_find), + cmocka_unit_test(test_uthash_find_missing), + cmocka_unit_test(test_uthash_del_reduces_count), + + /* config_struct defaults */ + cmocka_unit_test(test_config_has_device_0_default), + cmocka_unit_test(test_config_has_output_regex_default), + + /* regex_replace */ + cmocka_unit_test(test_regex_replace_numeric_extraction), + cmocka_unit_test(test_regex_replace_no_match_returns_input), + cmocka_unit_test(test_regex_replace_invalid_regex_returns_input), + cmocka_unit_test(test_regex_replace_full_match), + cmocka_unit_test(test_regex_replace_decimal_number), + + /* strncopy */ + cmocka_unit_test(test_strncopy_normal), + cmocka_unit_test(test_strncopy_truncates_at_n_minus_1), + cmocka_unit_test(test_strncopy_empty_src), + cmocka_unit_test(test_strncopy_exact_fit), + cmocka_unit_test(test_strncopy_returns_dst), + cmocka_unit_test(test_strncopy_zero_size), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/uthash.h b/uthash.h new file mode 100644 index 0000000..ac78fda --- /dev/null +++ b/uthash.h @@ -0,0 +1,1136 @@ +/* +Copyright (c) 2003-2021, Troy D. Hanson http://troydhanson.github.com/uthash/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +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 OWNER +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. +*/ + +#ifndef UTHASH_H +#define UTHASH_H + +#define UTHASH_VERSION 2.3.0 + +#include /* memcmp, memset, strlen */ +#include /* ptrdiff_t */ +#include /* exit */ + +#if defined(HASH_DEFINE_OWN_STDINT) && HASH_DEFINE_OWN_STDINT +/* This codepath is provided for backward compatibility, but I plan to remove it. */ +#warning "HASH_DEFINE_OWN_STDINT is deprecated; please use HASH_NO_STDINT instead" +typedef unsigned int uint32_t; +typedef unsigned char uint8_t; +#elif defined(HASH_NO_STDINT) && HASH_NO_STDINT +#else +#include /* uint8_t, uint32_t */ +#endif + +/* These macros use decltype or the earlier __typeof GNU extension. + As decltype is only available in newer compilers (VS2010 or gcc 4.3+ + when compiling c++ source) this code uses whatever method is needed + or, for VS2008 where neither is available, uses casting workarounds. */ +#if !defined(DECLTYPE) && !defined(NO_DECLTYPE) +#if defined(_MSC_VER) /* MS compiler */ +#if _MSC_VER >= 1600 && defined(__cplusplus) /* VS2010 or newer in C++ mode */ +#define DECLTYPE(x) (decltype(x)) +#else /* VS2008 or older (or VS2010 in C mode) */ +#define NO_DECLTYPE +#endif +#elif defined(__BORLANDC__) || defined(__ICCARM__) || defined(__LCC__) || defined(__WATCOMC__) +#define NO_DECLTYPE +#else /* GNU, Sun and other compilers */ +#define DECLTYPE(x) (__typeof(x)) +#endif +#endif + +#ifdef NO_DECLTYPE +#define DECLTYPE(x) +#define DECLTYPE_ASSIGN(dst,src) \ +do { \ + char **_da_dst = (char**)(&(dst)); \ + *_da_dst = (char*)(src); \ +} while (0) +#else +#define DECLTYPE_ASSIGN(dst,src) \ +do { \ + (dst) = DECLTYPE(dst)(src); \ +} while (0) +#endif + +#ifndef uthash_malloc +#define uthash_malloc(sz) malloc(sz) /* malloc fcn */ +#endif +#ifndef uthash_free +#define uthash_free(ptr,sz) free(ptr) /* free fcn */ +#endif +#ifndef uthash_bzero +#define uthash_bzero(a,n) memset(a,'\0',n) +#endif +#ifndef uthash_strlen +#define uthash_strlen(s) strlen(s) +#endif + +#ifndef HASH_FUNCTION +#define HASH_FUNCTION(keyptr,keylen,hashv) HASH_JEN(keyptr, keylen, hashv) +#endif + +#ifndef HASH_KEYCMP +#define HASH_KEYCMP(a,b,n) memcmp(a,b,n) +#endif + +#ifndef uthash_noexpand_fyi +#define uthash_noexpand_fyi(tbl) /* can be defined to log noexpand */ +#endif +#ifndef uthash_expand_fyi +#define uthash_expand_fyi(tbl) /* can be defined to log expands */ +#endif + +#ifndef HASH_NONFATAL_OOM +#define HASH_NONFATAL_OOM 0 +#endif + +#if HASH_NONFATAL_OOM +/* malloc failures can be recovered from */ + +#ifndef uthash_nonfatal_oom +#define uthash_nonfatal_oom(obj) do {} while (0) /* non-fatal OOM error */ +#endif + +#define HASH_RECORD_OOM(oomed) do { (oomed) = 1; } while (0) +#define IF_HASH_NONFATAL_OOM(x) x + +#else +/* malloc failures result in lost memory, hash tables are unusable */ + +#ifndef uthash_fatal +#define uthash_fatal(msg) exit(-1) /* fatal OOM error */ +#endif + +#define HASH_RECORD_OOM(oomed) uthash_fatal("out of memory") +#define IF_HASH_NONFATAL_OOM(x) + +#endif + +/* initial number of buckets */ +#define HASH_INITIAL_NUM_BUCKETS 32U /* initial number of buckets */ +#define HASH_INITIAL_NUM_BUCKETS_LOG2 5U /* lg2 of initial number of buckets */ +#define HASH_BKT_CAPACITY_THRESH 10U /* expand when bucket count reaches */ + +/* calculate the element whose hash handle address is hhp */ +#define ELMT_FROM_HH(tbl,hhp) ((void*)(((char*)(hhp)) - ((tbl)->hho))) +/* calculate the hash handle from element address elp */ +#define HH_FROM_ELMT(tbl,elp) ((UT_hash_handle*)(void*)(((char*)(elp)) + ((tbl)->hho))) + +#define HASH_ROLLBACK_BKT(hh, head, itemptrhh) \ +do { \ + struct UT_hash_handle *_hd_hh_item = (itemptrhh); \ + unsigned _hd_bkt; \ + HASH_TO_BKT(_hd_hh_item->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \ + (head)->hh.tbl->buckets[_hd_bkt].count++; \ + _hd_hh_item->hh_next = NULL; \ + _hd_hh_item->hh_prev = NULL; \ +} while (0) + +#define HASH_VALUE(keyptr,keylen,hashv) \ +do { \ + HASH_FUNCTION(keyptr, keylen, hashv); \ +} while (0) + +#define HASH_FIND_BYHASHVALUE(hh,head,keyptr,keylen,hashval,out) \ +do { \ + (out) = NULL; \ + if (head) { \ + unsigned _hf_bkt; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _hf_bkt); \ + if (HASH_BLOOM_TEST((head)->hh.tbl, hashval) != 0) { \ + HASH_FIND_IN_BKT((head)->hh.tbl, hh, (head)->hh.tbl->buckets[ _hf_bkt ], keyptr, keylen, hashval, out); \ + } \ + } \ +} while (0) + +#define HASH_FIND(hh,head,keyptr,keylen,out) \ +do { \ + (out) = NULL; \ + if (head) { \ + unsigned _hf_hashv; \ + HASH_VALUE(keyptr, keylen, _hf_hashv); \ + HASH_FIND_BYHASHVALUE(hh, head, keyptr, keylen, _hf_hashv, out); \ + } \ +} while (0) + +#ifdef HASH_BLOOM +#define HASH_BLOOM_BITLEN (1UL << HASH_BLOOM) +#define HASH_BLOOM_BYTELEN (HASH_BLOOM_BITLEN/8UL) + (((HASH_BLOOM_BITLEN%8UL)!=0UL) ? 1UL : 0UL) +#define HASH_BLOOM_MAKE(tbl,oomed) \ +do { \ + (tbl)->bloom_nbits = HASH_BLOOM; \ + (tbl)->bloom_bv = (uint8_t*)uthash_malloc(HASH_BLOOM_BYTELEN); \ + if (!(tbl)->bloom_bv) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \ + (tbl)->bloom_sig = HASH_BLOOM_SIGNATURE; \ + } \ +} while (0) + +#define HASH_BLOOM_FREE(tbl) \ +do { \ + uthash_free((tbl)->bloom_bv, HASH_BLOOM_BYTELEN); \ +} while (0) + +#define HASH_BLOOM_BITSET(bv,idx) (bv[(idx)/8U] |= (1U << ((idx)%8U))) +#define HASH_BLOOM_BITTEST(bv,idx) (bv[(idx)/8U] & (1U << ((idx)%8U))) + +#define HASH_BLOOM_ADD(tbl,hashv) \ + HASH_BLOOM_BITSET((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U))) + +#define HASH_BLOOM_TEST(tbl,hashv) \ + HASH_BLOOM_BITTEST((tbl)->bloom_bv, ((hashv) & (uint32_t)((1UL << (tbl)->bloom_nbits) - 1U))) + +#else +#define HASH_BLOOM_MAKE(tbl,oomed) +#define HASH_BLOOM_FREE(tbl) +#define HASH_BLOOM_ADD(tbl,hashv) +#define HASH_BLOOM_TEST(tbl,hashv) (1) +#define HASH_BLOOM_BYTELEN 0U +#endif + +#define HASH_MAKE_TABLE(hh,head,oomed) \ +do { \ + (head)->hh.tbl = (UT_hash_table*)uthash_malloc(sizeof(UT_hash_table)); \ + if (!(head)->hh.tbl) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head)->hh.tbl->tail = &((head)->hh); \ + (head)->hh.tbl->num_buckets = HASH_INITIAL_NUM_BUCKETS; \ + (head)->hh.tbl->log2_num_buckets = HASH_INITIAL_NUM_BUCKETS_LOG2; \ + (head)->hh.tbl->hho = (char*)(&(head)->hh) - (char*)(head); \ + (head)->hh.tbl->buckets = (UT_hash_bucket*)uthash_malloc( \ + HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \ + (head)->hh.tbl->signature = HASH_SIGNATURE; \ + if (!(head)->hh.tbl->buckets) { \ + HASH_RECORD_OOM(oomed); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + } else { \ + uthash_bzero((head)->hh.tbl->buckets, \ + HASH_INITIAL_NUM_BUCKETS * sizeof(struct UT_hash_bucket)); \ + HASH_BLOOM_MAKE((head)->hh.tbl, oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (oomed) { \ + uthash_free((head)->hh.tbl->buckets, \ + HASH_INITIAL_NUM_BUCKETS*sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + } \ + ) \ + } \ + } \ +} while (0) + +#define HASH_REPLACE_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,replaced,cmpfcn) \ +do { \ + (replaced) = NULL; \ + HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \ + if (replaced) { \ + HASH_DELETE(hh, head, replaced); \ + } \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn); \ +} while (0) + +#define HASH_REPLACE_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add,replaced) \ +do { \ + (replaced) = NULL; \ + HASH_FIND_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, replaced); \ + if (replaced) { \ + HASH_DELETE(hh, head, replaced); \ + } \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add); \ +} while (0) + +#define HASH_REPLACE(hh,head,fieldname,keylen_in,add,replaced) \ +do { \ + unsigned _hr_hashv; \ + HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \ + HASH_REPLACE_BYHASHVALUE(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced); \ +} while (0) + +#define HASH_REPLACE_INORDER(hh,head,fieldname,keylen_in,add,replaced,cmpfcn) \ +do { \ + unsigned _hr_hashv; \ + HASH_VALUE(&((add)->fieldname), keylen_in, _hr_hashv); \ + HASH_REPLACE_BYHASHVALUE_INORDER(hh, head, fieldname, keylen_in, _hr_hashv, add, replaced, cmpfcn); \ +} while (0) + +#define HASH_APPEND_LIST(hh, head, add) \ +do { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = ELMT_FROM_HH((head)->hh.tbl, (head)->hh.tbl->tail); \ + (head)->hh.tbl->tail->next = (add); \ + (head)->hh.tbl->tail = &((add)->hh); \ +} while (0) + +#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \ +do { \ + do { \ + if (cmpfcn(DECLTYPE(head)(_hs_iter), add) > 0) { \ + break; \ + } \ + } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \ +} while (0) + +#ifdef NO_DECLTYPE +#undef HASH_AKBI_INNER_LOOP +#define HASH_AKBI_INNER_LOOP(hh,head,add,cmpfcn) \ +do { \ + char *_hs_saved_head = (char*)(head); \ + do { \ + DECLTYPE_ASSIGN(head, _hs_iter); \ + if (cmpfcn(head, add) > 0) { \ + DECLTYPE_ASSIGN(head, _hs_saved_head); \ + break; \ + } \ + DECLTYPE_ASSIGN(head, _hs_saved_head); \ + } while ((_hs_iter = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->next)); \ +} while (0) +#endif + +#if HASH_NONFATAL_OOM + +#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \ +do { \ + if (!(oomed)) { \ + unsigned _ha_bkt; \ + (head)->hh.tbl->num_items++; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \ + HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \ + if (oomed) { \ + HASH_ROLLBACK_BKT(hh, head, &(add)->hh); \ + HASH_DELETE_HH(hh, head, &(add)->hh); \ + (add)->hh.tbl = NULL; \ + uthash_nonfatal_oom(add); \ + } else { \ + HASH_BLOOM_ADD((head)->hh.tbl, hashval); \ + HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \ + } \ + } else { \ + (add)->hh.tbl = NULL; \ + uthash_nonfatal_oom(add); \ + } \ +} while (0) + +#else + +#define HASH_ADD_TO_TABLE(hh,head,keyptr,keylen_in,hashval,add,oomed) \ +do { \ + unsigned _ha_bkt; \ + (head)->hh.tbl->num_items++; \ + HASH_TO_BKT(hashval, (head)->hh.tbl->num_buckets, _ha_bkt); \ + HASH_ADD_TO_BKT((head)->hh.tbl->buckets[_ha_bkt], hh, &(add)->hh, oomed); \ + HASH_BLOOM_ADD((head)->hh.tbl, hashval); \ + HASH_EMIT_KEY(hh, head, keyptr, keylen_in); \ +} while (0) + +#endif + + +#define HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh,head,keyptr,keylen_in,hashval,add,cmpfcn) \ +do { \ + IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \ + (add)->hh.hashv = (hashval); \ + (add)->hh.key = (char*) (keyptr); \ + (add)->hh.keylen = (unsigned) (keylen_in); \ + if (!(head)) { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = NULL; \ + HASH_MAKE_TABLE(hh, add, _ha_oomed); \ + IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \ + (head) = (add); \ + IF_HASH_NONFATAL_OOM( } ) \ + } else { \ + void *_hs_iter = (head); \ + (add)->hh.tbl = (head)->hh.tbl; \ + HASH_AKBI_INNER_LOOP(hh, head, add, cmpfcn); \ + if (_hs_iter) { \ + (add)->hh.next = _hs_iter; \ + if (((add)->hh.prev = HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev)) { \ + HH_FROM_ELMT((head)->hh.tbl, (add)->hh.prev)->next = (add); \ + } else { \ + (head) = (add); \ + } \ + HH_FROM_ELMT((head)->hh.tbl, _hs_iter)->prev = (add); \ + } else { \ + HASH_APPEND_LIST(hh, head, add); \ + } \ + } \ + HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \ + HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE_INORDER"); \ +} while (0) + +#define HASH_ADD_KEYPTR_INORDER(hh,head,keyptr,keylen_in,add,cmpfcn) \ +do { \ + unsigned _hs_hashv; \ + HASH_VALUE(keyptr, keylen_in, _hs_hashv); \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, keyptr, keylen_in, _hs_hashv, add, cmpfcn); \ +} while (0) + +#define HASH_ADD_BYHASHVALUE_INORDER(hh,head,fieldname,keylen_in,hashval,add,cmpfcn) \ + HASH_ADD_KEYPTR_BYHASHVALUE_INORDER(hh, head, &((add)->fieldname), keylen_in, hashval, add, cmpfcn) + +#define HASH_ADD_INORDER(hh,head,fieldname,keylen_in,add,cmpfcn) \ + HASH_ADD_KEYPTR_INORDER(hh, head, &((add)->fieldname), keylen_in, add, cmpfcn) + +#define HASH_ADD_KEYPTR_BYHASHVALUE(hh,head,keyptr,keylen_in,hashval,add) \ +do { \ + IF_HASH_NONFATAL_OOM( int _ha_oomed = 0; ) \ + (add)->hh.hashv = (hashval); \ + (add)->hh.key = (const void*) (keyptr); \ + (add)->hh.keylen = (unsigned) (keylen_in); \ + if (!(head)) { \ + (add)->hh.next = NULL; \ + (add)->hh.prev = NULL; \ + HASH_MAKE_TABLE(hh, add, _ha_oomed); \ + IF_HASH_NONFATAL_OOM( if (!_ha_oomed) { ) \ + (head) = (add); \ + IF_HASH_NONFATAL_OOM( } ) \ + } else { \ + (add)->hh.tbl = (head)->hh.tbl; \ + HASH_APPEND_LIST(hh, head, add); \ + } \ + HASH_ADD_TO_TABLE(hh, head, keyptr, keylen_in, hashval, add, _ha_oomed); \ + HASH_FSCK(hh, head, "HASH_ADD_KEYPTR_BYHASHVALUE"); \ +} while (0) + +#define HASH_ADD_KEYPTR(hh,head,keyptr,keylen_in,add) \ +do { \ + unsigned _ha_hashv; \ + HASH_VALUE(keyptr, keylen_in, _ha_hashv); \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, keyptr, keylen_in, _ha_hashv, add); \ +} while (0) + +#define HASH_ADD_BYHASHVALUE(hh,head,fieldname,keylen_in,hashval,add) \ + HASH_ADD_KEYPTR_BYHASHVALUE(hh, head, &((add)->fieldname), keylen_in, hashval, add) + +#define HASH_ADD(hh,head,fieldname,keylen_in,add) \ + HASH_ADD_KEYPTR(hh, head, &((add)->fieldname), keylen_in, add) + +#define HASH_TO_BKT(hashv,num_bkts,bkt) \ +do { \ + bkt = ((hashv) & ((num_bkts) - 1U)); \ +} while (0) + +/* delete "delptr" from the hash table. + * "the usual" patch-up process for the app-order doubly-linked-list. + * The use of _hd_hh_del below deserves special explanation. + * These used to be expressed using (delptr) but that led to a bug + * if someone used the same symbol for the head and deletee, like + * HASH_DELETE(hh,users,users); + * We want that to work, but by changing the head (users) below + * we were forfeiting our ability to further refer to the deletee (users) + * in the patch-up process. Solution: use scratch space to + * copy the deletee pointer, then the latter references are via that + * scratch pointer rather than through the repointed (users) symbol. + */ +#define HASH_DELETE(hh,head,delptr) \ + HASH_DELETE_HH(hh, head, &(delptr)->hh) + +#define HASH_DELETE_HH(hh,head,delptrhh) \ +do { \ + struct UT_hash_handle *_hd_hh_del = (delptrhh); \ + if ((_hd_hh_del->prev == NULL) && (_hd_hh_del->next == NULL)) { \ + HASH_BLOOM_FREE((head)->hh.tbl); \ + uthash_free((head)->hh.tbl->buckets, \ + (head)->hh.tbl->num_buckets * sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head) = NULL; \ + } else { \ + unsigned _hd_bkt; \ + if (_hd_hh_del == (head)->hh.tbl->tail) { \ + (head)->hh.tbl->tail = HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev); \ + } \ + if (_hd_hh_del->prev != NULL) { \ + HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->prev)->next = _hd_hh_del->next; \ + } else { \ + DECLTYPE_ASSIGN(head, _hd_hh_del->next); \ + } \ + if (_hd_hh_del->next != NULL) { \ + HH_FROM_ELMT((head)->hh.tbl, _hd_hh_del->next)->prev = _hd_hh_del->prev; \ + } \ + HASH_TO_BKT(_hd_hh_del->hashv, (head)->hh.tbl->num_buckets, _hd_bkt); \ + HASH_DEL_IN_BKT((head)->hh.tbl->buckets[_hd_bkt], _hd_hh_del); \ + (head)->hh.tbl->num_items--; \ + } \ + HASH_FSCK(hh, head, "HASH_DELETE_HH"); \ +} while (0) + +/* convenience forms of HASH_FIND/HASH_ADD/HASH_DEL */ +#define HASH_FIND_STR(head,findstr,out) \ +do { \ + unsigned _uthash_hfstr_keylen = (unsigned)uthash_strlen(findstr); \ + HASH_FIND(hh, head, findstr, _uthash_hfstr_keylen, out); \ +} while (0) +#define HASH_ADD_STR(head,strfield,add) \ +do { \ + unsigned _uthash_hastr_keylen = (unsigned)uthash_strlen((add)->strfield); \ + HASH_ADD(hh, head, strfield[0], _uthash_hastr_keylen, add); \ +} while (0) +#define HASH_REPLACE_STR(head,strfield,add,replaced) \ +do { \ + unsigned _uthash_hrstr_keylen = (unsigned)uthash_strlen((add)->strfield); \ + HASH_REPLACE(hh, head, strfield[0], _uthash_hrstr_keylen, add, replaced); \ +} while (0) +#define HASH_FIND_INT(head,findint,out) \ + HASH_FIND(hh,head,findint,sizeof(int),out) +#define HASH_ADD_INT(head,intfield,add) \ + HASH_ADD(hh,head,intfield,sizeof(int),add) +#define HASH_REPLACE_INT(head,intfield,add,replaced) \ + HASH_REPLACE(hh,head,intfield,sizeof(int),add,replaced) +#define HASH_FIND_PTR(head,findptr,out) \ + HASH_FIND(hh,head,findptr,sizeof(void *),out) +#define HASH_ADD_PTR(head,ptrfield,add) \ + HASH_ADD(hh,head,ptrfield,sizeof(void *),add) +#define HASH_REPLACE_PTR(head,ptrfield,add,replaced) \ + HASH_REPLACE(hh,head,ptrfield,sizeof(void *),add,replaced) +#define HASH_DEL(head,delptr) \ + HASH_DELETE(hh,head,delptr) + +/* HASH_FSCK checks hash integrity on every add/delete when HASH_DEBUG is defined. + * This is for uthash developer only; it compiles away if HASH_DEBUG isn't defined. + */ +#ifdef HASH_DEBUG +#include /* fprintf, stderr */ +#define HASH_OOPS(...) do { fprintf(stderr, __VA_ARGS__); exit(-1); } while (0) +#define HASH_FSCK(hh,head,where) \ +do { \ + struct UT_hash_handle *_thh; \ + if (head) { \ + unsigned _bkt_i; \ + unsigned _count = 0; \ + char *_prev; \ + for (_bkt_i = 0; _bkt_i < (head)->hh.tbl->num_buckets; ++_bkt_i) { \ + unsigned _bkt_count = 0; \ + _thh = (head)->hh.tbl->buckets[_bkt_i].hh_head; \ + _prev = NULL; \ + while (_thh) { \ + if (_prev != (char*)(_thh->hh_prev)) { \ + HASH_OOPS("%s: invalid hh_prev %p, actual %p\n", \ + (where), (void*)_thh->hh_prev, (void*)_prev); \ + } \ + _bkt_count++; \ + _prev = (char*)(_thh); \ + _thh = _thh->hh_next; \ + } \ + _count += _bkt_count; \ + if ((head)->hh.tbl->buckets[_bkt_i].count != _bkt_count) { \ + HASH_OOPS("%s: invalid bucket count %u, actual %u\n", \ + (where), (head)->hh.tbl->buckets[_bkt_i].count, _bkt_count); \ + } \ + } \ + if (_count != (head)->hh.tbl->num_items) { \ + HASH_OOPS("%s: invalid hh item count %u, actual %u\n", \ + (where), (head)->hh.tbl->num_items, _count); \ + } \ + _count = 0; \ + _prev = NULL; \ + _thh = &(head)->hh; \ + while (_thh) { \ + _count++; \ + if (_prev != (char*)_thh->prev) { \ + HASH_OOPS("%s: invalid prev %p, actual %p\n", \ + (where), (void*)_thh->prev, (void*)_prev); \ + } \ + _prev = (char*)ELMT_FROM_HH((head)->hh.tbl, _thh); \ + _thh = (_thh->next ? HH_FROM_ELMT((head)->hh.tbl, _thh->next) : NULL); \ + } \ + if (_count != (head)->hh.tbl->num_items) { \ + HASH_OOPS("%s: invalid app item count %u, actual %u\n", \ + (where), (head)->hh.tbl->num_items, _count); \ + } \ + } \ +} while (0) +#else +#define HASH_FSCK(hh,head,where) +#endif + +/* When compiled with -DHASH_EMIT_KEYS, length-prefixed keys are emitted to + * the descriptor to which this macro is defined for tuning the hash function. + * The app can #include to get the prototype for write(2). */ +#ifdef HASH_EMIT_KEYS +#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen) \ +do { \ + unsigned _klen = fieldlen; \ + write(HASH_EMIT_KEYS, &_klen, sizeof(_klen)); \ + write(HASH_EMIT_KEYS, keyptr, (unsigned long)fieldlen); \ +} while (0) +#else +#define HASH_EMIT_KEY(hh,head,keyptr,fieldlen) +#endif + +/* The Bernstein hash function, used in Perl prior to v5.6. Note (x<<5+x)=x*33. */ +#define HASH_BER(key,keylen,hashv) \ +do { \ + unsigned _hb_keylen = (unsigned)keylen; \ + const unsigned char *_hb_key = (const unsigned char*)(key); \ + (hashv) = 0; \ + while (_hb_keylen-- != 0U) { \ + (hashv) = (((hashv) << 5) + (hashv)) + *_hb_key++; \ + } \ +} while (0) + + +/* SAX/FNV/OAT/JEN hash functions are macro variants of those listed at + * http://eternallyconfuzzled.com/tuts/algorithms/jsw_tut_hashing.aspx */ +#define HASH_SAX(key,keylen,hashv) \ +do { \ + unsigned _sx_i; \ + const unsigned char *_hs_key = (const unsigned char*)(key); \ + hashv = 0; \ + for (_sx_i=0; _sx_i < keylen; _sx_i++) { \ + hashv ^= (hashv << 5) + (hashv >> 2) + _hs_key[_sx_i]; \ + } \ +} while (0) +/* FNV-1a variation */ +#define HASH_FNV(key,keylen,hashv) \ +do { \ + unsigned _fn_i; \ + const unsigned char *_hf_key = (const unsigned char*)(key); \ + (hashv) = 2166136261U; \ + for (_fn_i=0; _fn_i < keylen; _fn_i++) { \ + hashv = hashv ^ _hf_key[_fn_i]; \ + hashv = hashv * 16777619U; \ + } \ +} while (0) + +#define HASH_OAT(key,keylen,hashv) \ +do { \ + unsigned _ho_i; \ + const unsigned char *_ho_key=(const unsigned char*)(key); \ + hashv = 0; \ + for(_ho_i=0; _ho_i < keylen; _ho_i++) { \ + hashv += _ho_key[_ho_i]; \ + hashv += (hashv << 10); \ + hashv ^= (hashv >> 6); \ + } \ + hashv += (hashv << 3); \ + hashv ^= (hashv >> 11); \ + hashv += (hashv << 15); \ +} while (0) + +#define HASH_JEN_MIX(a,b,c) \ +do { \ + a -= b; a -= c; a ^= ( c >> 13 ); \ + b -= c; b -= a; b ^= ( a << 8 ); \ + c -= a; c -= b; c ^= ( b >> 13 ); \ + a -= b; a -= c; a ^= ( c >> 12 ); \ + b -= c; b -= a; b ^= ( a << 16 ); \ + c -= a; c -= b; c ^= ( b >> 5 ); \ + a -= b; a -= c; a ^= ( c >> 3 ); \ + b -= c; b -= a; b ^= ( a << 10 ); \ + c -= a; c -= b; c ^= ( b >> 15 ); \ +} while (0) + +#define HASH_JEN(key,keylen,hashv) \ +do { \ + unsigned _hj_i,_hj_j,_hj_k; \ + unsigned const char *_hj_key=(unsigned const char*)(key); \ + hashv = 0xfeedbeefu; \ + _hj_i = _hj_j = 0x9e3779b9u; \ + _hj_k = (unsigned)(keylen); \ + while (_hj_k >= 12U) { \ + _hj_i += (_hj_key[0] + ( (unsigned)_hj_key[1] << 8 ) \ + + ( (unsigned)_hj_key[2] << 16 ) \ + + ( (unsigned)_hj_key[3] << 24 ) ); \ + _hj_j += (_hj_key[4] + ( (unsigned)_hj_key[5] << 8 ) \ + + ( (unsigned)_hj_key[6] << 16 ) \ + + ( (unsigned)_hj_key[7] << 24 ) ); \ + hashv += (_hj_key[8] + ( (unsigned)_hj_key[9] << 8 ) \ + + ( (unsigned)_hj_key[10] << 16 ) \ + + ( (unsigned)_hj_key[11] << 24 ) ); \ + \ + HASH_JEN_MIX(_hj_i, _hj_j, hashv); \ + \ + _hj_key += 12; \ + _hj_k -= 12U; \ + } \ + hashv += (unsigned)(keylen); \ + switch ( _hj_k ) { \ + case 11: hashv += ( (unsigned)_hj_key[10] << 24 ); /* FALLTHROUGH */ \ + case 10: hashv += ( (unsigned)_hj_key[9] << 16 ); /* FALLTHROUGH */ \ + case 9: hashv += ( (unsigned)_hj_key[8] << 8 ); /* FALLTHROUGH */ \ + case 8: _hj_j += ( (unsigned)_hj_key[7] << 24 ); /* FALLTHROUGH */ \ + case 7: _hj_j += ( (unsigned)_hj_key[6] << 16 ); /* FALLTHROUGH */ \ + case 6: _hj_j += ( (unsigned)_hj_key[5] << 8 ); /* FALLTHROUGH */ \ + case 5: _hj_j += _hj_key[4]; /* FALLTHROUGH */ \ + case 4: _hj_i += ( (unsigned)_hj_key[3] << 24 ); /* FALLTHROUGH */ \ + case 3: _hj_i += ( (unsigned)_hj_key[2] << 16 ); /* FALLTHROUGH */ \ + case 2: _hj_i += ( (unsigned)_hj_key[1] << 8 ); /* FALLTHROUGH */ \ + case 1: _hj_i += _hj_key[0]; /* FALLTHROUGH */ \ + default: ; \ + } \ + HASH_JEN_MIX(_hj_i, _hj_j, hashv); \ +} while (0) + +/* The Paul Hsieh hash function */ +#undef get16bits +#if (defined(__GNUC__) && defined(__i386__)) || defined(__WATCOMC__) \ + || defined(_MSC_VER) || defined (__BORLANDC__) || defined (__TURBOC__) +#define get16bits(d) (*((const uint16_t *) (d))) +#endif + +#if !defined (get16bits) +#define get16bits(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8) \ + +(uint32_t)(((const uint8_t *)(d))[0]) ) +#endif +#define HASH_SFH(key,keylen,hashv) \ +do { \ + unsigned const char *_sfh_key=(unsigned const char*)(key); \ + uint32_t _sfh_tmp, _sfh_len = (uint32_t)keylen; \ + \ + unsigned _sfh_rem = _sfh_len & 3U; \ + _sfh_len >>= 2; \ + hashv = 0xcafebabeu; \ + \ + /* Main loop */ \ + for (;_sfh_len > 0U; _sfh_len--) { \ + hashv += get16bits (_sfh_key); \ + _sfh_tmp = ((uint32_t)(get16bits (_sfh_key+2)) << 11) ^ hashv; \ + hashv = (hashv << 16) ^ _sfh_tmp; \ + _sfh_key += 2U*sizeof (uint16_t); \ + hashv += hashv >> 11; \ + } \ + \ + /* Handle end cases */ \ + switch (_sfh_rem) { \ + case 3: hashv += get16bits (_sfh_key); \ + hashv ^= hashv << 16; \ + hashv ^= (uint32_t)(_sfh_key[sizeof (uint16_t)]) << 18; \ + hashv += hashv >> 11; \ + break; \ + case 2: hashv += get16bits (_sfh_key); \ + hashv ^= hashv << 11; \ + hashv += hashv >> 17; \ + break; \ + case 1: hashv += *_sfh_key; \ + hashv ^= hashv << 10; \ + hashv += hashv >> 1; \ + break; \ + default: ; \ + } \ + \ + /* Force "avalanching" of final 127 bits */ \ + hashv ^= hashv << 3; \ + hashv += hashv >> 5; \ + hashv ^= hashv << 4; \ + hashv += hashv >> 17; \ + hashv ^= hashv << 25; \ + hashv += hashv >> 6; \ +} while (0) + +/* iterate over items in a known bucket to find desired item */ +#define HASH_FIND_IN_BKT(tbl,hh,head,keyptr,keylen_in,hashval,out) \ +do { \ + if ((head).hh_head != NULL) { \ + DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (head).hh_head)); \ + } else { \ + (out) = NULL; \ + } \ + while ((out) != NULL) { \ + if ((out)->hh.hashv == (hashval) && (out)->hh.keylen == (keylen_in)) { \ + if (HASH_KEYCMP((out)->hh.key, keyptr, keylen_in) == 0) { \ + break; \ + } \ + } \ + if ((out)->hh.hh_next != NULL) { \ + DECLTYPE_ASSIGN(out, ELMT_FROM_HH(tbl, (out)->hh.hh_next)); \ + } else { \ + (out) = NULL; \ + } \ + } \ +} while (0) + +/* add an item to a bucket */ +#define HASH_ADD_TO_BKT(head,hh,addhh,oomed) \ +do { \ + UT_hash_bucket *_ha_head = &(head); \ + _ha_head->count++; \ + (addhh)->hh_next = _ha_head->hh_head; \ + (addhh)->hh_prev = NULL; \ + if (_ha_head->hh_head != NULL) { \ + _ha_head->hh_head->hh_prev = (addhh); \ + } \ + _ha_head->hh_head = (addhh); \ + if ((_ha_head->count >= ((_ha_head->expand_mult + 1U) * HASH_BKT_CAPACITY_THRESH)) \ + && !(addhh)->tbl->noexpand) { \ + HASH_EXPAND_BUCKETS(addhh,(addhh)->tbl, oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (oomed) { \ + HASH_DEL_IN_BKT(head,addhh); \ + } \ + ) \ + } \ +} while (0) + +/* remove an item from a given bucket */ +#define HASH_DEL_IN_BKT(head,delhh) \ +do { \ + UT_hash_bucket *_hd_head = &(head); \ + _hd_head->count--; \ + if (_hd_head->hh_head == (delhh)) { \ + _hd_head->hh_head = (delhh)->hh_next; \ + } \ + if ((delhh)->hh_prev) { \ + (delhh)->hh_prev->hh_next = (delhh)->hh_next; \ + } \ + if ((delhh)->hh_next) { \ + (delhh)->hh_next->hh_prev = (delhh)->hh_prev; \ + } \ +} while (0) + +/* Bucket expansion has the effect of doubling the number of buckets + * and redistributing the items into the new buckets. Ideally the + * items will distribute more or less evenly into the new buckets + * (the extent to which this is true is a measure of the quality of + * the hash function as it applies to the key domain). + * + * With the items distributed into more buckets, the chain length + * (item count) in each bucket is reduced. Thus by expanding buckets + * the hash keeps a bound on the chain length. This bounded chain + * length is the essence of how a hash provides constant time lookup. + * + * The calculation of tbl->ideal_chain_maxlen below deserves some + * explanation. First, keep in mind that we're calculating the ideal + * maximum chain length based on the *new* (doubled) bucket count. + * In fractions this is just n/b (n=number of items,b=new num buckets). + * Since the ideal chain length is an integer, we want to calculate + * ceil(n/b). We don't depend on floating point arithmetic in this + * hash, so to calculate ceil(n/b) with integers we could write + * + * ceil(n/b) = (n/b) + ((n%b)?1:0) + * + * and in fact a previous version of this hash did just that. + * But now we have improved things a bit by recognizing that b is + * always a power of two. We keep its base 2 log handy (call it lb), + * so now we can write this with a bit shift and logical AND: + * + * ceil(n/b) = (n>>lb) + ( (n & (b-1)) ? 1:0) + * + */ +#define HASH_EXPAND_BUCKETS(hh,tbl,oomed) \ +do { \ + unsigned _he_bkt; \ + unsigned _he_bkt_i; \ + struct UT_hash_handle *_he_thh, *_he_hh_nxt; \ + UT_hash_bucket *_he_new_buckets, *_he_newbkt; \ + _he_new_buckets = (UT_hash_bucket*)uthash_malloc( \ + sizeof(struct UT_hash_bucket) * (tbl)->num_buckets * 2U); \ + if (!_he_new_buckets) { \ + HASH_RECORD_OOM(oomed); \ + } else { \ + uthash_bzero(_he_new_buckets, \ + sizeof(struct UT_hash_bucket) * (tbl)->num_buckets * 2U); \ + (tbl)->ideal_chain_maxlen = \ + ((tbl)->num_items >> ((tbl)->log2_num_buckets+1U)) + \ + ((((tbl)->num_items & (((tbl)->num_buckets*2U)-1U)) != 0U) ? 1U : 0U); \ + (tbl)->nonideal_items = 0; \ + for (_he_bkt_i = 0; _he_bkt_i < (tbl)->num_buckets; _he_bkt_i++) { \ + _he_thh = (tbl)->buckets[ _he_bkt_i ].hh_head; \ + while (_he_thh != NULL) { \ + _he_hh_nxt = _he_thh->hh_next; \ + HASH_TO_BKT(_he_thh->hashv, (tbl)->num_buckets * 2U, _he_bkt); \ + _he_newbkt = &(_he_new_buckets[_he_bkt]); \ + if (++(_he_newbkt->count) > (tbl)->ideal_chain_maxlen) { \ + (tbl)->nonideal_items++; \ + if (_he_newbkt->count > _he_newbkt->expand_mult * (tbl)->ideal_chain_maxlen) { \ + _he_newbkt->expand_mult++; \ + } \ + } \ + _he_thh->hh_prev = NULL; \ + _he_thh->hh_next = _he_newbkt->hh_head; \ + if (_he_newbkt->hh_head != NULL) { \ + _he_newbkt->hh_head->hh_prev = _he_thh; \ + } \ + _he_newbkt->hh_head = _he_thh; \ + _he_thh = _he_hh_nxt; \ + } \ + } \ + uthash_free((tbl)->buckets, (tbl)->num_buckets * sizeof(struct UT_hash_bucket)); \ + (tbl)->num_buckets *= 2U; \ + (tbl)->log2_num_buckets++; \ + (tbl)->buckets = _he_new_buckets; \ + (tbl)->ineff_expands = ((tbl)->nonideal_items > ((tbl)->num_items >> 1)) ? \ + ((tbl)->ineff_expands+1U) : 0U; \ + if ((tbl)->ineff_expands > 1U) { \ + (tbl)->noexpand = 1; \ + uthash_noexpand_fyi(tbl); \ + } \ + uthash_expand_fyi(tbl); \ + } \ +} while (0) + + +/* This is an adaptation of Simon Tatham's O(n log(n)) mergesort */ +/* Note that HASH_SORT assumes the hash handle name to be hh. + * HASH_SRT was added to allow the hash handle name to be passed in. */ +#define HASH_SORT(head,cmpfcn) HASH_SRT(hh,head,cmpfcn) +#define HASH_SRT(hh,head,cmpfcn) \ +do { \ + unsigned _hs_i; \ + unsigned _hs_looping,_hs_nmerges,_hs_insize,_hs_psize,_hs_qsize; \ + struct UT_hash_handle *_hs_p, *_hs_q, *_hs_e, *_hs_list, *_hs_tail; \ + if (head != NULL) { \ + _hs_insize = 1; \ + _hs_looping = 1; \ + _hs_list = &((head)->hh); \ + while (_hs_looping != 0U) { \ + _hs_p = _hs_list; \ + _hs_list = NULL; \ + _hs_tail = NULL; \ + _hs_nmerges = 0; \ + while (_hs_p != NULL) { \ + _hs_nmerges++; \ + _hs_q = _hs_p; \ + _hs_psize = 0; \ + for (_hs_i = 0; _hs_i < _hs_insize; ++_hs_i) { \ + _hs_psize++; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + if (_hs_q == NULL) { \ + break; \ + } \ + } \ + _hs_qsize = _hs_insize; \ + while ((_hs_psize != 0U) || ((_hs_qsize != 0U) && (_hs_q != NULL))) { \ + if (_hs_psize == 0U) { \ + _hs_e = _hs_q; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + _hs_qsize--; \ + } else if ((_hs_qsize == 0U) || (_hs_q == NULL)) { \ + _hs_e = _hs_p; \ + if (_hs_p != NULL) { \ + _hs_p = ((_hs_p->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \ + } \ + _hs_psize--; \ + } else if ((cmpfcn( \ + DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_p)), \ + DECLTYPE(head)(ELMT_FROM_HH((head)->hh.tbl, _hs_q)) \ + )) <= 0) { \ + _hs_e = _hs_p; \ + if (_hs_p != NULL) { \ + _hs_p = ((_hs_p->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_p->next) : NULL); \ + } \ + _hs_psize--; \ + } else { \ + _hs_e = _hs_q; \ + _hs_q = ((_hs_q->next != NULL) ? \ + HH_FROM_ELMT((head)->hh.tbl, _hs_q->next) : NULL); \ + _hs_qsize--; \ + } \ + if ( _hs_tail != NULL ) { \ + _hs_tail->next = ((_hs_e != NULL) ? \ + ELMT_FROM_HH((head)->hh.tbl, _hs_e) : NULL); \ + } else { \ + _hs_list = _hs_e; \ + } \ + if (_hs_e != NULL) { \ + _hs_e->prev = ((_hs_tail != NULL) ? \ + ELMT_FROM_HH((head)->hh.tbl, _hs_tail) : NULL); \ + } \ + _hs_tail = _hs_e; \ + } \ + _hs_p = _hs_q; \ + } \ + if (_hs_tail != NULL) { \ + _hs_tail->next = NULL; \ + } \ + if (_hs_nmerges <= 1U) { \ + _hs_looping = 0; \ + (head)->hh.tbl->tail = _hs_tail; \ + DECLTYPE_ASSIGN(head, ELMT_FROM_HH((head)->hh.tbl, _hs_list)); \ + } \ + _hs_insize *= 2U; \ + } \ + HASH_FSCK(hh, head, "HASH_SRT"); \ + } \ +} while (0) + +/* This function selects items from one hash into another hash. + * The end result is that the selected items have dual presence + * in both hashes. There is no copy of the items made; rather + * they are added into the new hash through a secondary hash + * hash handle that must be present in the structure. */ +#define HASH_SELECT(hh_dst, dst, hh_src, src, cond) \ +do { \ + unsigned _src_bkt, _dst_bkt; \ + void *_last_elt = NULL, *_elt; \ + UT_hash_handle *_src_hh, *_dst_hh, *_last_elt_hh=NULL; \ + ptrdiff_t _dst_hho = ((char*)(&(dst)->hh_dst) - (char*)(dst)); \ + if ((src) != NULL) { \ + for (_src_bkt=0; _src_bkt < (src)->hh_src.tbl->num_buckets; _src_bkt++) { \ + for (_src_hh = (src)->hh_src.tbl->buckets[_src_bkt].hh_head; \ + _src_hh != NULL; \ + _src_hh = _src_hh->hh_next) { \ + _elt = ELMT_FROM_HH((src)->hh_src.tbl, _src_hh); \ + if (cond(_elt)) { \ + IF_HASH_NONFATAL_OOM( int _hs_oomed = 0; ) \ + _dst_hh = (UT_hash_handle*)(void*)(((char*)_elt) + _dst_hho); \ + _dst_hh->key = _src_hh->key; \ + _dst_hh->keylen = _src_hh->keylen; \ + _dst_hh->hashv = _src_hh->hashv; \ + _dst_hh->prev = _last_elt; \ + _dst_hh->next = NULL; \ + if (_last_elt_hh != NULL) { \ + _last_elt_hh->next = _elt; \ + } \ + if ((dst) == NULL) { \ + DECLTYPE_ASSIGN(dst, _elt); \ + HASH_MAKE_TABLE(hh_dst, dst, _hs_oomed); \ + IF_HASH_NONFATAL_OOM( \ + if (_hs_oomed) { \ + uthash_nonfatal_oom(_elt); \ + (dst) = NULL; \ + continue; \ + } \ + ) \ + } else { \ + _dst_hh->tbl = (dst)->hh_dst.tbl; \ + } \ + HASH_TO_BKT(_dst_hh->hashv, _dst_hh->tbl->num_buckets, _dst_bkt); \ + HASH_ADD_TO_BKT(_dst_hh->tbl->buckets[_dst_bkt], hh_dst, _dst_hh, _hs_oomed); \ + (dst)->hh_dst.tbl->num_items++; \ + IF_HASH_NONFATAL_OOM( \ + if (_hs_oomed) { \ + HASH_ROLLBACK_BKT(hh_dst, dst, _dst_hh); \ + HASH_DELETE_HH(hh_dst, dst, _dst_hh); \ + _dst_hh->tbl = NULL; \ + uthash_nonfatal_oom(_elt); \ + continue; \ + } \ + ) \ + HASH_BLOOM_ADD(_dst_hh->tbl, _dst_hh->hashv); \ + _last_elt = _elt; \ + _last_elt_hh = _dst_hh; \ + } \ + } \ + } \ + } \ + HASH_FSCK(hh_dst, dst, "HASH_SELECT"); \ +} while (0) + +#define HASH_CLEAR(hh,head) \ +do { \ + if ((head) != NULL) { \ + HASH_BLOOM_FREE((head)->hh.tbl); \ + uthash_free((head)->hh.tbl->buckets, \ + (head)->hh.tbl->num_buckets*sizeof(struct UT_hash_bucket)); \ + uthash_free((head)->hh.tbl, sizeof(UT_hash_table)); \ + (head) = NULL; \ + } \ +} while (0) + +#define HASH_OVERHEAD(hh,head) \ + (((head) != NULL) ? ( \ + (size_t)(((head)->hh.tbl->num_items * sizeof(UT_hash_handle)) + \ + ((head)->hh.tbl->num_buckets * sizeof(UT_hash_bucket)) + \ + sizeof(UT_hash_table) + \ + (HASH_BLOOM_BYTELEN))) : 0U) + +#ifdef NO_DECLTYPE +#define HASH_ITER(hh,head,el,tmp) \ +for(((el)=(head)), ((*(char**)(&(tmp)))=(char*)((head!=NULL)?(head)->hh.next:NULL)); \ + (el) != NULL; ((el)=(tmp)), ((*(char**)(&(tmp)))=(char*)((tmp!=NULL)?(tmp)->hh.next:NULL))) +#else +#define HASH_ITER(hh,head,el,tmp) \ +for(((el)=(head)), ((tmp)=DECLTYPE(el)((head!=NULL)?(head)->hh.next:NULL)); \ + (el) != NULL; ((el)=(tmp)), ((tmp)=DECLTYPE(el)((tmp!=NULL)?(tmp)->hh.next:NULL))) +#endif + +/* obtain a count of items in the hash */ +#define HASH_COUNT(head) HASH_CNT(hh,head) +#define HASH_CNT(hh,head) ((head != NULL)?((head)->hh.tbl->num_items):0U) + +typedef struct UT_hash_bucket { + struct UT_hash_handle *hh_head; + unsigned count; + + /* expand_mult is normally set to 0. In this situation, the max chain length + * threshold is enforced at its default value, HASH_BKT_CAPACITY_THRESH. (If + * the bucket's chain exceeds this length, bucket expansion is triggered). + * However, setting expand_mult to a non-zero value delays bucket expansion + * (that would be triggered by additions to this particular bucket) + * until its chain length reaches a *multiple* of HASH_BKT_CAPACITY_THRESH. + * (The multiplier is simply expand_mult+1). The whole idea of this + * multiplier is to reduce bucket expansions, since they are expensive, in + * situations where we know that a particular bucket tends to be overused. + * It is better to let its chain length grow to a longer yet-still-bounded + * value, than to do an O(n) bucket expansion too often. + */ + unsigned expand_mult; + +} UT_hash_bucket; + +/* random signature used only to find hash tables in external analysis */ +#define HASH_SIGNATURE 0xa0111fe1u +#define HASH_BLOOM_SIGNATURE 0xb12220f2u + +typedef struct UT_hash_table { + UT_hash_bucket *buckets; + unsigned num_buckets, log2_num_buckets; + unsigned num_items; + struct UT_hash_handle *tail; /* tail hh in app order, for fast append */ + ptrdiff_t hho; /* hash handle offset (byte pos of hash handle in element */ + + /* in an ideal situation (all buckets used equally), no bucket would have + * more than ceil(#items/#buckets) items. that's the ideal chain length. */ + unsigned ideal_chain_maxlen; + + /* nonideal_items is the number of items in the hash whose chain position + * exceeds the ideal chain maxlen. these items pay the penalty for an uneven + * hash distribution; reaching them in a chain traversal takes >ideal steps */ + unsigned nonideal_items; + + /* ineffective expands occur when a bucket doubling was performed, but + * afterward, more than half the items in the hash had nonideal chain + * positions. If this happens on two consecutive expansions we inhibit any + * further expansion, as it's not helping; this happens when the hash + * function isn't a good fit for the key domain. When expansion is inhibited + * the hash will still work, albeit no longer in constant time. */ + unsigned ineff_expands, noexpand; + + uint32_t signature; /* used only to find hash tables in external analysis */ +#ifdef HASH_BLOOM + uint32_t bloom_sig; /* used only to test bloom exists in external analysis */ + uint8_t *bloom_bv; + uint8_t bloom_nbits; +#endif + +} UT_hash_table; + +typedef struct UT_hash_handle { + struct UT_hash_table *tbl; + void *prev; /* prev element in app order */ + void *next; /* next element in app order */ + struct UT_hash_handle *hh_prev; /* previous hh in bucket order */ + struct UT_hash_handle *hh_next; /* next hh in bucket order */ + const void *key; /* ptr to enclosing struct's key */ + unsigned keylen; /* enclosing struct's key len */ + unsigned hashv; /* result of hash-fcn(key) */ +} UT_hash_handle; + +#endif /* UTHASH_H */