diff --git a/libbpf-tools/Makefile b/libbpf-tools/Makefile index bebf6bdb6b6d..71ed474fff05 100644 --- a/libbpf-tools/Makefile +++ b/libbpf-tools/Makefile @@ -116,6 +116,7 @@ COMMON_OBJ = \ $(OUTPUT)/btf_helpers.o \ $(OUTPUT)/compat.o \ $(OUTPUT)/path_helpers.o \ + $(OUTPUT)/datastructure_helpers.o \ $(if $(ENABLE_MIN_CORE_BTFS),$(OUTPUT)/min_core_btf_tar.o) \ # diff --git a/libbpf-tools/biotop.c b/libbpf-tools/biotop.c index dc8e65970605..1d99b06ac72d 100644 --- a/libbpf-tools/biotop.c +++ b/libbpf-tools/biotop.c @@ -27,6 +27,7 @@ #include "compat.h" #include "trace_helpers.h" #include "map_helpers.h" +#include "datastructure_helpers.h" #define warn(...) fprintf(stderr, __VA_ARGS__) #define OUTPUT_ROWS_LIMIT 10240 @@ -44,39 +45,13 @@ struct disk { char name[256]; }; -struct vector { - size_t nr; - size_t capacity; - void **elems; +struct disk_key { + int major; + int minor; }; -int grow_vector(struct vector *vector) { - if (vector->nr >= vector->capacity) { - void **reallocated; - - if (!vector->capacity) - vector->capacity = 1; - else - vector->capacity *= 2; - - reallocated = libbpf_reallocarray(vector->elems, vector->capacity, sizeof(*vector->elems)); - if (!reallocated) - return -1; - - vector->elems = reallocated; - } - - return 0; -} - -void free_vector(struct vector vector) { - for (size_t i = 0; i < vector.nr; i++) - if (vector.elems[i] != NULL) - free(vector.elems[i]); - free(vector.elems); -} - -struct vector disks = {}; +static struct ds_vec disks = {}; +static struct ds_hashmap disk_map = {}; static volatile sig_atomic_t exiting = 0; @@ -232,48 +207,50 @@ static void parse_disk_stat(void) if (!fp) return; + ds_vec_init(&disks, sizeof(struct disk)); + if (ds_hashmap_init(&disk_map, sizeof(struct disk_key), + sizeof(((struct disk *)0)->name), 0)) { + fclose(fp); + return; + } + zero = 0; while (getline(&line, &zero, fp) != -1) { struct disk disk; + struct disk_key key; if (sscanf(line, "%d %d %s", &disk.major, &disk.minor, disk.name) != 3) continue; - if (grow_vector(&disks) == -1) + if (ds_vec_push_back(&disks, &disk)) goto err; - disks.elems[disks.nr] = malloc(sizeof(disk)); - if (!disks.elems[disks.nr]) + key.major = disk.major; + key.minor = disk.minor; + if (ds_hashmap_insert(&disk_map, &key, disk.name)) goto err; - - memcpy(disks.elems[disks.nr], &disk, sizeof(disk)); - - disks.nr++; } free(line); fclose(fp); - return; -err: - fprintf(stderr, "realloc or malloc failed\n"); - free_vector(disks); +err: + fprintf(stderr, "allocation failed while parsing disk stats\n"); + free(line); + fclose(fp); + ds_vec_free(&disks); + ds_hashmap_free(&disk_map); } static char *search_disk_name(int major, int minor) { - for (size_t i = 0; i < disks.nr; i++) { - struct disk *diskp; - - if (!disks.elems[i]) - continue; - - diskp = (struct disk *) disks.elems[i]; - if (diskp->major == major && diskp->minor == minor) - return diskp->name; - } + struct disk_key key = { .major = major, .minor = minor }; + char *name; + name = ds_hashmap_find(&disk_map, &key); + if (name) + return name; return ""; } @@ -456,7 +433,8 @@ int main(int argc, char **argv) cleanup: ksyms__free(ksyms); - free_vector(disks); + ds_vec_free(&disks); + ds_hashmap_free(&disk_map); biotop_bpf__destroy(obj); return err != 0; diff --git a/libbpf-tools/datastructure_helpers.c b/libbpf-tools/datastructure_helpers.c new file mode 100644 index 000000000000..b5e1dbd98234 --- /dev/null +++ b/libbpf-tools/datastructure_helpers.c @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2026 LG Electronics Inc. +#include +#include +#include +#include +#include +#include "datastructure_helpers.h" + +/* + * FNV-1a 64-bit constants (CC0 1.0 Public Domain). + * + * FNV_OFFSET_BASIS: the FNV-1a 64-bit offset basis (0xcbf29ce484222325). + * Chosen empirically by the FNV authors for good avalanche properties. + * + * FNV_PRIME: the FNV-1a 64-bit prime (0x00000100000001b3 = 2^40 + 2^8 + 0xb3). + * A prime of this specific form ensures wide bit diffusion after each + * multiply step. + */ +#define FNV_OFFSET_BASIS 14695981039346656037ULL +#define FNV_PRIME 1099511628211ULL + +/* + * HASH_COMBINE_GOLDEN: 2^64 / phi, where phi is the golden ratio. + * Adding this irrational-derived constant before XORing spreads hash + * values uniformly across the 64-bit space (boost::hash_combine idiom). + */ +#define HASH_COMBINE_GOLDEN 0x9e3779b97f4a7c15ULL + +/* + * NODE_KEY / NODE_VAL: access the key and value stored inline after the + * ds_hashmap_node header. These must be macros rather than functions + * because struct ds_hashmap_node is an opaque type in the header; its + * full definition (and therefore sizeof) is only available inside this + * translation unit. Moving the layout arithmetic into a function would + * require exposing the struct definition in the header. + */ +#define NODE_KEY(n) ((void *)((n) + 1)) +#define NODE_VAL(n, ks) ((void *)((char *)((n) + 1) + (ks))) + +/* ============================================================ + * Vector implementation + * ============================================================ */ + +void ds_vec_init(struct ds_vec *v, size_t elem_size) +{ + memset(v, 0, sizeof(*v)); + v->elem_size = elem_size; +} + +void ds_vec_free(struct ds_vec *v) +{ + free(v->data); + v->data = NULL; + v->nr = 0; + v->capacity = 0; +} + +int ds_vec_reserve(struct ds_vec *v, size_t cap) +{ + void *tmp; + + if (cap <= v->capacity) + return 0; + + tmp = realloc(v->data, cap * v->elem_size); + if (!tmp) + return -ENOMEM; + + v->data = tmp; + v->capacity = cap; + + return 0; +} + +int ds_vec_push_back(struct ds_vec *v, const void *elem) +{ + if (v->nr >= v->capacity) { + size_t new_cap = v->capacity ? v->capacity * 2 + : DS_VEC_INIT_CAP; + if (ds_vec_reserve(v, new_cap)) + return -ENOMEM; + } + + memcpy((char *)v->data + v->nr * v->elem_size, elem, v->elem_size); + v->nr++; + + return 0; +} + +int ds_vec_pop_back(struct ds_vec *v, void *out) +{ + if (!v->nr) + return -1; + + v->nr--; + if (out) + memcpy(out, (char *)v->data + v->nr * v->elem_size, + v->elem_size); + return 0; +} + +void *ds_vec_at(struct ds_vec *v, size_t idx) +{ + if (idx >= v->nr) + return NULL; + + return (char *)v->data + idx * v->elem_size; +} + +void ds_vec_clear(struct ds_vec *v) +{ + v->nr = 0; +} + +void ds_vec_sort(struct ds_vec *v, int (*cmp)(const void *, const void *)) +{ + if (v->nr > 1) + qsort(v->data, v->nr, v->elem_size, cmp); +} + +size_t ds_vec_size(const struct ds_vec *v) +{ + return v->nr; +} + +bool ds_vec_empty(const struct ds_vec *v) +{ + return v->nr == 0; +} + +/* ============================================================ + * Hash helpers + * ============================================================ */ + +/* + * FNV-1a hash for arbitrary bytes. + */ +unsigned long ds_hash_bytes(const void *key, size_t len) +{ + const unsigned char *p = key; + unsigned long h = FNV_OFFSET_BASIS; + size_t i; + + for (i = 0; i < len; i++) { + h ^= p[i]; + h *= FNV_PRIME; + } + + return h; +} + +/* + * FNV-1a hash for null-terminated strings. + */ +unsigned long ds_hash_string(const char *str) +{ + unsigned long h = FNV_OFFSET_BASIS; + + while (*str) { + h ^= (unsigned char)*str++; + h *= FNV_PRIME; + } + + return h; +} + +/* + * Combine two hash values (boost-style). + */ +unsigned long ds_hash_combine(unsigned long h1, unsigned long h2) +{ + h1 ^= h2 + HASH_COMBINE_GOLDEN + (h1 << 6) + (h1 >> 2); + + return h1; +} + +/* ============================================================ + * Hashmap implementation (separate chaining) + * ============================================================ + * + * Each bucket holds a singly-linked list of nodes. Every node + * stores its key and value inline, immediately after the header: + * + * [struct ds_hashmap_node] [key (key_size bytes)] [val (val_size bytes)] + * + * The bucket array doubles when the average chain length exceeds 2, + * keeping expected lookup O(1). + */ + +struct ds_hashmap_node { + struct ds_hashmap_node *next; +}; + +static struct ds_hashmap_node *hashmap__alloc_node(const struct ds_hashmap *m, + const void *key, const void *val) +{ + struct ds_hashmap_node *node; + + node = malloc(sizeof(*node) + m->key_stride + m->val_size); + if (!node) + return NULL; + + node->next = NULL; + memcpy(NODE_KEY(node), key, m->key_size); + memcpy(NODE_VAL(node, m->key_stride), val, m->val_size); + + return node; +} + +static size_t hashmap__bucket(const struct ds_hashmap *m, const void *key) +{ + return ds_hash_bytes(key, m->key_size) & (m->capacity - 1); +} + +static int hashmap__grow(struct ds_hashmap *m) +{ + struct ds_hashmap_node **new_buckets; + size_t new_cap = m->capacity * 2; + size_t i; + + new_buckets = calloc(new_cap, sizeof(*new_buckets)); + if (!new_buckets) + return -ENOMEM; + + /* Relink all existing nodes into the new bucket array */ + for (i = 0; i < m->capacity; i++) { + struct ds_hashmap_node *node = m->buckets[i]; + + while (node) { + struct ds_hashmap_node *next = node->next; + size_t b = ds_hash_bytes(NODE_KEY(node), m->key_size) + & (new_cap - 1); + + node->next = new_buckets[b]; + new_buckets[b] = node; + node = next; + } + } + + free(m->buckets); + m->buckets = new_buckets; + m->capacity = new_cap; + + return 0; +} + +int ds_hashmap_init(struct ds_hashmap *m, size_t key_size, size_t val_size, + size_t init_cap) +{ + size_t real_cap; + + memset(m, 0, sizeof(*m)); + m->key_size = key_size; + m->val_size = val_size; + /* + * Round key_size up to max_align_t so that NODE_VAL is always + * suitably aligned for any value type. key_stride is used only + * for pointer arithmetic and allocation; key_size (the original + * value) continues to be used for memcpy and memcmp so that we + * never read bytes beyond the key object the caller passed. + */ + m->key_stride = (key_size + _Alignof(max_align_t) - 1) + & ~(_Alignof(max_align_t) - 1); + + if (init_cap < DS_HASHMAP_INIT_CAP) + init_cap = DS_HASHMAP_INIT_CAP; + + /* Round up to power of 2 */ + real_cap = 1; + while (real_cap < init_cap) + real_cap <<= 1; + + m->buckets = calloc(real_cap, sizeof(*m->buckets)); + if (!m->buckets) + return -ENOMEM; + + m->capacity = real_cap; + + return 0; +} + +void ds_hashmap_free(struct ds_hashmap *m) +{ + ds_hashmap_clear(m); + free(m->buckets); + m->buckets = NULL; + m->capacity = 0; +} + +int ds_hashmap_insert(struct ds_hashmap *m, const void *key, const void *val) +{ + struct ds_hashmap_node *node; + size_t b; + + /* Grow when average chain length exceeds 2 */ + if (m->count > 2 * m->capacity) { + if (hashmap__grow(m)) + return -ENOMEM; + } + + b = hashmap__bucket(m, key); + node = m->buckets[b]; + + /* Walk chain: update value if key already present */ + while (node) { + if (!memcmp(NODE_KEY(node), key, m->key_size)) { + memcpy(NODE_VAL(node, m->key_stride), val, m->val_size); + return 0; + } + node = node->next; + } + + /* Key not found: prepend a new node to the bucket */ + node = hashmap__alloc_node(m, key, val); + if (!node) + return -ENOMEM; + node->next = m->buckets[b]; + m->buckets[b] = node; + m->count++; + return 0; +} + +void *ds_hashmap_find(struct ds_hashmap *m, const void *key) +{ + struct ds_hashmap_node *node; + + if (!m->capacity) + return NULL; + + node = m->buckets[hashmap__bucket(m, key)]; + while (node) { + if (!memcmp(NODE_KEY(node), key, m->key_size)) + return NODE_VAL(node, m->key_stride); + node = node->next; + } + return NULL; +} + +int ds_hashmap_delete(struct ds_hashmap *m, const void *key) +{ + struct ds_hashmap_node **pp; + + if (!m->capacity) + return -1; + + pp = &m->buckets[hashmap__bucket(m, key)]; + while (*pp) { + if (!memcmp(NODE_KEY(*pp), key, m->key_size)) { + struct ds_hashmap_node *del = *pp; + + *pp = del->next; + free(del); + m->count--; + return 0; + } + pp = &(*pp)->next; + } + return -1; +} + +bool ds_hashmap_next(struct ds_hashmap *m, struct ds_hashmap_iter *it, + void **key_out, void **val_out) +{ + struct ds_hashmap_node *node = it->node; + + /* Advance past empty buckets if no current node */ + while (!node && it->idx < m->capacity) + node = m->buckets[it->idx++]; + + if (!node) + return false; + + if (key_out) + *key_out = NODE_KEY(node); + if (val_out) + *val_out = NODE_VAL(node, m->key_stride); + it->node = node->next; + + return true; +} + +void ds_hashmap_clear(struct ds_hashmap *m) +{ + size_t i; + + for (i = 0; i < m->capacity; i++) { + struct ds_hashmap_node *node = m->buckets[i]; + + while (node) { + struct ds_hashmap_node *next = node->next; + + free(node); + node = next; + } + m->buckets[i] = NULL; + } + m->count = 0; +} + +size_t ds_hashmap_count(const struct ds_hashmap *m) +{ + return m->count; +} diff --git a/libbpf-tools/datastructure_helpers.h b/libbpf-tools/datastructure_helpers.h new file mode 100644 index 000000000000..a1fb42c2870f --- /dev/null +++ b/libbpf-tools/datastructure_helpers.h @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +/* Copyright (c) 2026 LG Electronics Inc. */ +#ifndef __DATASTRUCTURE_HELPERS_H +#define __DATASTRUCTURE_HELPERS_H + +#include +#include + +#define DS_VEC_INIT_CAP 16 +#define DS_HASHMAP_INIT_CAP 64 + +/* + * ============================================================ + * Generic dynamic array (vector) + * ============================================================ + * + * A realloc-based dynamic array storing element data inline. + * Each element is elem_size bytes. Growth factor is 2x, + * giving amortized O(1) push_back. + * + * Usage: + * + * struct ds_vec v = {}; + * ds_vec_init(&v, sizeof(int)); + * + * int val = 42; + * ds_vec_push_back(&v, &val); + * + * int *p = ds_vec_at(&v, 0); + * printf("%d\n", *p); // 42 + * printf("size=%zu\n", v.nr); + * + * ds_vec_sort(&v, int_cmp); + * ds_vec_free(&v); + */ + +struct ds_vec { + void *data; + size_t nr; + size_t capacity; + size_t elem_size; +}; + +void ds_vec_init(struct ds_vec *v, size_t elem_size); +void ds_vec_free(struct ds_vec *v); +int ds_vec_reserve(struct ds_vec *v, size_t cap); +int ds_vec_push_back(struct ds_vec *v, const void *elem); +int ds_vec_pop_back(struct ds_vec *v, void *out); +void *ds_vec_at(struct ds_vec *v, size_t idx); +void ds_vec_clear(struct ds_vec *v); +void ds_vec_sort(struct ds_vec *v, int (*cmp)(const void *, const void *)); +size_t ds_vec_size(const struct ds_vec *v); +bool ds_vec_empty(const struct ds_vec *v); + +/* + * ============================================================ + * Generic hash map + * ============================================================ + * + * Separate-chaining hash map. Each bucket holds a singly-linked + * list of nodes; every node stores its key and value inline. + * Uses FNV-1a for hashing. Bucket array doubles when the average + * chain length exceeds 2. + * + * Key padding rule: + * Keys are compared with memcmp(). If the key type is a struct with + * padding bytes, those bytes must be zeroed before use or lookups may + * silently fail. Always initialise struct keys with memset before + * filling their fields: + * + * struct my_key key; + * memset(&key, 0, sizeof(key)); + * key.field = value; + * + * This is the same requirement as for Linux eBPF map keys. + * + * Usage: + * + * struct ds_hashmap map = {}; + * ds_hashmap_init(&map, sizeof(__u64), sizeof(struct my_val), 0); + * + * __u64 key = 123; + * struct my_val val = { .count = 1 }; + * ds_hashmap_insert(&map, &key, &val); + * + * struct my_val *found = ds_hashmap_find(&map, &key); + * if (found) + * found->count++; + * + * ds_hashmap_delete(&map, &key); + * + * // Iterate all entries: + * struct ds_hashmap_iter it = {}; + * void *k, *v; + * while (ds_hashmap_next(&map, &it, &k, &v)) { + * __u64 *kp = k; + * struct my_val *vp = v; + * printf("key=%llu count=%d\n", *kp, vp->count); + * } + * + * ds_hashmap_free(&map); + */ + +/* Opaque node type; defined in datastructure_helpers.c */ +struct ds_hashmap_node; + +struct ds_hashmap { + struct ds_hashmap_node **buckets; /* array of chain head pointers */ + size_t capacity; /* number of buckets (power of 2) */ + size_t count; + size_t key_size; /* original key size for memcpy/memcmp */ + size_t key_stride; /* key_size rounded up to max alignment for NODE_VAL offset */ + size_t val_size; +}; + +struct ds_hashmap_iter { + size_t idx; /* next bucket to scan */ + void *node; /* next node in current chain; NULL to start at idx */ +}; + +int ds_hashmap_init(struct ds_hashmap *m, size_t key_size, size_t val_size, + size_t init_cap); +void ds_hashmap_free(struct ds_hashmap *m); +int ds_hashmap_insert(struct ds_hashmap *m, const void *key, const void *val); +void *ds_hashmap_find(struct ds_hashmap *m, const void *key); +int ds_hashmap_delete(struct ds_hashmap *m, const void *key); +bool ds_hashmap_next(struct ds_hashmap *m, struct ds_hashmap_iter *it, + void **key_out, void **val_out); +void ds_hashmap_clear(struct ds_hashmap *m); +size_t ds_hashmap_count(const struct ds_hashmap *m); + +/* Utility hash functions */ +unsigned long ds_hash_bytes(const void *key, size_t len); +unsigned long ds_hash_string(const char *str); +unsigned long ds_hash_combine(unsigned long h1, unsigned long h2); + +#endif /* __DATASTRUCTURE_HELPERS_H */ diff --git a/libbpf-tools/tests/.gitignore b/libbpf-tools/tests/.gitignore new file mode 100644 index 000000000000..acd3cc058702 --- /dev/null +++ b/libbpf-tools/tests/.gitignore @@ -0,0 +1 @@ +test_datastructure_helpers diff --git a/libbpf-tools/tests/Makefile b/libbpf-tools/tests/Makefile new file mode 100644 index 000000000000..0fe7fb0af26a --- /dev/null +++ b/libbpf-tools/tests/Makefile @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) + +CC ?= cc +CFLAGS := -Wall -Wextra -Werror -g -I.. + +TESTS := test_datastructure_helpers + +.PHONY: all clean test + +all: $(TESTS) + +test_datastructure_helpers: test_datastructure_helpers.c ../datastructure_helpers.c + $(CC) $(CFLAGS) -o $@ $^ + +test: all + @failed=0; \ + for t in $(TESTS); do \ + echo "Running $$t ..."; \ + ./$$t || failed=$$((failed + 1)); \ + done; \ + if [ $$failed -ne 0 ]; then \ + echo "$$failed test(s) FAILED"; \ + exit 1; \ + fi + +clean: + $(RM) $(TESTS) diff --git a/libbpf-tools/tests/test_datastructure_helpers.c b/libbpf-tools/tests/test_datastructure_helpers.c new file mode 100644 index 000000000000..9802c8c32fb4 --- /dev/null +++ b/libbpf-tools/tests/test_datastructure_helpers.c @@ -0,0 +1,581 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2026 LG Electronics Inc. + +/* + * test_datastructure_helpers.c - Unit tests for datastructure_helpers. + * + * Build and run (no BPF or kernel required): + * cc -o test_datastructure_helpers \ + * test_datastructure_helpers.c datastructure_helpers.c && \ + * ./test_datastructure_helpers + */ + +#include +#include +#include +#include +#include +#include +#include "datastructure_helpers.h" + +/* ============================================================ + * Minimal test framework + * ============================================================ */ + +static int g_tests_run; +static int g_tests_failed; +static const char *g_current_suite; + +#define SUITE(name) \ + do { g_current_suite = (name); printf("\n[%s]\n", name); } while (0) + +#define CHECK(expr) \ + do { \ + g_tests_run++; \ + if (!(expr)) { \ + g_tests_failed++; \ + fprintf(stderr, " FAIL %s:%d: %s\n", \ + __FILE__, __LINE__, #expr); \ + } else { \ + printf(" PASS %s\n", #expr); \ + } \ + } while (0) + +/* ============================================================ + * vec tests + * ============================================================ */ + +static int int_cmp_asc(const void *a, const void *b) +{ + int aa = *(const int *)a; + int bb = *(const int *)b; + + return (aa > bb) - (aa < bb); +} + +static void test_vec_basic(void) +{ + struct ds_vec v = {}; + int val; + + SUITE("vec: basic push_back / at / size / empty"); + + ds_vec_init(&v, sizeof(int)); + CHECK(ds_vec_empty(&v)); + CHECK(ds_vec_size(&v) == 0); + CHECK(ds_vec_at(&v, 0) == NULL); + + val = 10; + CHECK(ds_vec_push_back(&v, &val) == 0); + val = 20; + CHECK(ds_vec_push_back(&v, &val) == 0); + val = 30; + CHECK(ds_vec_push_back(&v, &val) == 0); + + CHECK(!ds_vec_empty(&v)); + CHECK(ds_vec_size(&v) == 3); + CHECK(*(int *)ds_vec_at(&v, 0) == 10); + CHECK(*(int *)ds_vec_at(&v, 1) == 20); + CHECK(*(int *)ds_vec_at(&v, 2) == 30); + CHECK(ds_vec_at(&v, 3) == NULL); + + ds_vec_free(&v); + CHECK(ds_vec_size(&v) == 0); + CHECK(v.data == NULL); +} + +static void test_vec_pop_back(void) +{ + struct ds_vec v = {}; + int out = 0; + + SUITE("vec: pop_back"); + + ds_vec_init(&v, sizeof(int)); + + /* pop on empty */ + CHECK(ds_vec_pop_back(&v, &out) == -1); + + int val = 42; + ds_vec_push_back(&v, &val); + val = 99; + ds_vec_push_back(&v, &val); + + CHECK(ds_vec_pop_back(&v, &out) == 0); + CHECK(out == 99); + CHECK(ds_vec_size(&v) == 1); + + CHECK(ds_vec_pop_back(&v, &out) == 0); + CHECK(out == 42); + CHECK(ds_vec_empty(&v)); + + CHECK(ds_vec_pop_back(&v, &out) == -1); + + ds_vec_free(&v); +} + +static void test_vec_growth(void) +{ + struct ds_vec v = {}; + int i, val; + + SUITE("vec: growth past DS_VEC_INIT_CAP"); + + ds_vec_init(&v, sizeof(int)); + + /* Push more than DS_VEC_INIT_CAP (16) elements to force realloc */ + for (i = 0; i < 64; i++) { + val = i * 3; + CHECK(ds_vec_push_back(&v, &val) == 0); + } + + CHECK(ds_vec_size(&v) == 64); + for (i = 0; i < 64; i++) + CHECK(*(int *)ds_vec_at(&v, i) == i * 3); + + ds_vec_free(&v); +} + +static void test_vec_sort(void) +{ + struct ds_vec v = {}; + int vals[] = {5, 2, 8, 1, 9, 3}; + int expected[] = {1, 2, 3, 5, 8, 9}; + int i; + + SUITE("vec: sort"); + + ds_vec_init(&v, sizeof(int)); + for (i = 0; i < 6; i++) + ds_vec_push_back(&v, &vals[i]); + + ds_vec_sort(&v, int_cmp_asc); + + for (i = 0; i < 6; i++) + CHECK(*(int *)ds_vec_at(&v, i) == expected[i]); + + ds_vec_free(&v); +} + +static void test_vec_clear(void) +{ + struct ds_vec v = {}; + int val = 1; + + SUITE("vec: clear keeps capacity"); + + ds_vec_init(&v, sizeof(int)); + ds_vec_push_back(&v, &val); + ds_vec_push_back(&v, &val); + + size_t cap_before = v.capacity; + ds_vec_clear(&v); + CHECK(ds_vec_size(&v) == 0); + CHECK(v.capacity == cap_before); /* capacity is preserved */ + + ds_vec_free(&v); +} + +static void test_vec_reserve(void) +{ + struct ds_vec v = {}; + + SUITE("vec: reserve"); + + ds_vec_init(&v, sizeof(int)); + CHECK(ds_vec_reserve(&v, 128) == 0); + CHECK(v.capacity >= 128); + CHECK(v.nr == 0); + + /* reserve smaller than current capacity is no-op */ + size_t old_cap = v.capacity; + CHECK(ds_vec_reserve(&v, 1) == 0); + CHECK(v.capacity == old_cap); + + ds_vec_free(&v); +} + +static void test_vec_struct_elements(void) +{ + struct point { int x; int y; }; + struct ds_vec v = {}; + struct point p; + + SUITE("vec: struct elements"); + + ds_vec_init(&v, sizeof(struct point)); + + p.x = 1; p.y = 2; ds_vec_push_back(&v, &p); + p.x = 3; p.y = 4; ds_vec_push_back(&v, &p); + p.x = 5; p.y = 6; ds_vec_push_back(&v, &p); + + CHECK(((struct point *)ds_vec_at(&v, 0))->x == 1); + CHECK(((struct point *)ds_vec_at(&v, 0))->y == 2); + CHECK(((struct point *)ds_vec_at(&v, 2))->x == 5); + CHECK(((struct point *)ds_vec_at(&v, 2))->y == 6); + + ds_vec_free(&v); +} + +/* ============================================================ + * hashmap tests + * ============================================================ */ + +static void test_hashmap_basic(void) +{ + struct ds_hashmap m = {}; + int key, val; + int *found; + + SUITE("hashmap: basic insert / find / delete"); + + CHECK(ds_hashmap_init(&m, sizeof(int), sizeof(int), 0) == 0); + CHECK(ds_hashmap_count(&m) == 0); + + key = 1; val = 100; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + key = 2; val = 200; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + key = 3; val = 300; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + + CHECK(ds_hashmap_count(&m) == 3); + + key = 1; + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && *found == 100); + + key = 2; + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && *found == 200); + + key = 3; + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && *found == 300); + + /* key not present */ + key = 99; + CHECK(ds_hashmap_find(&m, &key) == NULL); + + /* delete */ + key = 2; + CHECK(ds_hashmap_delete(&m, &key) == 0); + CHECK(ds_hashmap_count(&m) == 2); + CHECK(ds_hashmap_find(&m, &key) == NULL); + + /* delete again (not present) */ + CHECK(ds_hashmap_delete(&m, &key) == -1); + + ds_hashmap_free(&m); +} + +static void test_hashmap_update(void) +{ + struct ds_hashmap m = {}; + int key = 42, val; + int *found; + + SUITE("hashmap: insert updates existing key"); + + CHECK(ds_hashmap_init(&m, sizeof(int), sizeof(int), 0) == 0); + + val = 1; + ds_hashmap_insert(&m, &key, &val); + val = 2; + ds_hashmap_insert(&m, &key, &val); /* update */ + + CHECK(ds_hashmap_count(&m) == 1); + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && *found == 2); + + ds_hashmap_free(&m); +} + +static void test_hashmap_iteration(void) +{ + struct ds_hashmap m = {}; + struct ds_hashmap_iter it = {}; + int key, val; + void *k, *v; + int sum_keys = 0, sum_vals = 0; + int i; + + SUITE("hashmap: iteration"); + + CHECK(ds_hashmap_init(&m, sizeof(int), sizeof(int), 0) == 0); + + for (i = 1; i <= 5; i++) { + key = i; + val = i * 10; + ds_hashmap_insert(&m, &key, &val); + } + + while (ds_hashmap_next(&m, &it, &k, &v)) { + sum_keys += *(int *)k; + sum_vals += *(int *)v; + } + + /* 1+2+3+4+5 = 15, 10+20+30+40+50 = 150 */ + CHECK(sum_keys == 15); + CHECK(sum_vals == 150); + + ds_hashmap_free(&m); +} + +static void test_hashmap_clear(void) +{ + struct ds_hashmap m = {}; + int key = 1, val = 1; + + SUITE("hashmap: clear"); + + CHECK(ds_hashmap_init(&m, sizeof(int), sizeof(int), 0) == 0); + + ds_hashmap_insert(&m, &key, &val); + ds_hashmap_insert(&m, &key, &val); + + ds_hashmap_clear(&m); + CHECK(ds_hashmap_count(&m) == 0); + + key = 1; + CHECK(ds_hashmap_find(&m, &key) == NULL); + + ds_hashmap_free(&m); +} + +static void test_hashmap_rehash(void) +{ + struct ds_hashmap m = {}; + int key, val, i; + int *found; + + SUITE("hashmap: rehash on high load"); + + /* Start with a small capacity to force rehash */ + CHECK(ds_hashmap_init(&m, sizeof(int), sizeof(int), 4) == 0); + + /* Insert more than 2x capacity entries to trigger bucket array growth */ + for (i = 0; i < 200; i++) { + key = i; + val = i * 7; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + } + + CHECK(ds_hashmap_count(&m) == 200); + + /* All entries must still be retrievable after rehash */ + for (i = 0; i < 200; i++) { + key = i; + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && *found == i * 7); + } + + ds_hashmap_free(&m); +} + +static void test_hashmap_u64_key(void) +{ + struct ds_hashmap m = {}; + typedef unsigned long long u64; + u64 key; + struct { u64 size; int count; } val, *found; + + SUITE("hashmap: u64 key with struct value"); + + CHECK(ds_hashmap_init(&m, sizeof(u64), sizeof(val), 0) == 0); + + key = 0xdeadbeefcafeULL; + val.size = 1024; val.count = 3; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + + key = 0x1234567890abcdefULL; + val.size = 512; val.count = 1; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + + key = 0xdeadbeefcafeULL; + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && found->size == 1024 && found->count == 3); + + key = 0x1234567890abcdefULL; + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && found->size == 512 && found->count == 1); + + ds_hashmap_free(&m); +} + +static void test_hashmap_char_key_u64_val(void) +{ + struct ds_hashmap m = {}; + struct ds_hashmap_iter it = {}; + char key; + unsigned long long val, *found; + void *kp, *vp; + int i; + + SUITE("hashmap: char key with uint64_t value"); + + CHECK(ds_hashmap_init(&m, sizeof(char), sizeof(unsigned long long), 0) == 0); + + for (i = 0; i < 5; i++) { + key = (char)('a' + i); + val = (unsigned long long)i * 1000000000ULL; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + } + + for (i = 0; i < 5; i++) { + key = (char)('a' + i); + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL); + CHECK(*found == (unsigned long long)i * 1000000000ULL); + /* Value pointer must satisfy the alignment of uint64_t */ + CHECK(((uintptr_t)found % _Alignof(unsigned long long)) == 0); + } + + /* Same alignment check during iteration */ + while (ds_hashmap_next(&m, &it, &kp, &vp)) + CHECK(((uintptr_t)vp % _Alignof(unsigned long long)) == 0); + + ds_hashmap_free(&m); +} + +static void test_hashmap_struct_key_padding(void) +{ + struct padded_key { + char tag; /* 1 byte */ + /* 3 bytes padding on all common 32/64-bit ABIs */ + int id; /* 4 bytes */ + }; + struct ds_hashmap m = {}; + struct padded_key stored, lookup; + int val = 42, *found; + + SUITE("hashmap: struct key with padding bytes"); + + CHECK(ds_hashmap_init(&m, sizeof(struct padded_key), sizeof(int), 0) == 0); + + /* + * Keys are compared with memcmp(). Struct keys must be zeroed before + * filling fields so that padding bytes are consistent across lookups. + */ + + /* Correct usage: memset before filling fields */ + memset(&stored, 0, sizeof(stored)); + stored.tag = 'X'; + stored.id = 99; + val = 42; + CHECK(ds_hashmap_insert(&m, &stored, &val) == 0); + + memset(&lookup, 0, sizeof(lookup)); + lookup.tag = 'X'; + lookup.id = 99; + found = ds_hashmap_find(&m, &lookup); + CHECK(found != NULL && *found == 42); + + /* Without memset, garbage in padding bytes causes a lookup miss */ + memset(&lookup, 0xAB, sizeof(lookup)); + lookup.tag = 'X'; + lookup.id = 99; + found = ds_hashmap_find(&m, &lookup); + CHECK(found == NULL); + + ds_hashmap_free(&m); +} + +static void test_hashmap_delete_reinsert(void) +{ + struct ds_hashmap m = {}; + int key, val; + int *found; + + SUITE("hashmap: delete then reinsert same key"); + + CHECK(ds_hashmap_init(&m, sizeof(int), sizeof(int), 0) == 0); + + key = 7; val = 70; + ds_hashmap_insert(&m, &key, &val); + ds_hashmap_delete(&m, &key); + + /* Reinsert after delete */ + val = 77; + CHECK(ds_hashmap_insert(&m, &key, &val) == 0); + found = ds_hashmap_find(&m, &key); + CHECK(found != NULL && *found == 77); + CHECK(ds_hashmap_count(&m) == 1); + + ds_hashmap_free(&m); +} + +/* ============================================================ + * Hash utility tests + * ============================================================ */ + +static void test_hash_utilities(void) +{ + SUITE("hash utilities"); + + /* Same bytes produce same hash */ + int a = 42, b = 42; + CHECK(ds_hash_bytes(&a, sizeof(a)) == ds_hash_bytes(&b, sizeof(b))); + + /* Different bytes produce different hash (with high probability) */ + int c = 43; + CHECK(ds_hash_bytes(&a, sizeof(a)) != ds_hash_bytes(&c, sizeof(c))); + + /* Same string produces same hash */ + CHECK(ds_hash_string("hello") == ds_hash_string("hello")); + + /* Different strings produce different hash (with high probability) */ + CHECK(ds_hash_string("hello") != ds_hash_string("world")); + + /* Empty string hash is defined and stable */ + unsigned long h1 = ds_hash_string(""); + unsigned long h2 = ds_hash_string(""); + CHECK(h1 == h2); + + /* combine is deterministic */ + unsigned long c1 = ds_hash_combine(0xabcdUL, 0x1234UL); + unsigned long c2 = ds_hash_combine(0xabcdUL, 0x1234UL); + CHECK(c1 == c2); + + /* combine(a,b) != combine(b,a) in general (order matters) */ + unsigned long c3 = ds_hash_combine(0x1234UL, 0xabcdUL); + CHECK(c1 != c3); +} + +/* ============================================================ + * main + * ============================================================ */ + +int main(void) +{ + test_vec_basic(); + test_vec_pop_back(); + test_vec_growth(); + test_vec_sort(); + test_vec_clear(); + test_vec_reserve(); + test_vec_struct_elements(); + + test_hashmap_basic(); + test_hashmap_update(); + test_hashmap_iteration(); + test_hashmap_clear(); + test_hashmap_rehash(); + test_hashmap_u64_key(); + test_hashmap_char_key_u64_val(); + test_hashmap_struct_key_padding(); + test_hashmap_delete_reinsert(); + + test_hash_utilities(); + + printf("\n========================================\n"); + printf("Results: %d/%d tests passed\n", + g_tests_run - g_tests_failed, g_tests_run); + if (g_tests_failed) + printf("FAILED: %d test(s) failed\n", g_tests_failed); + else + printf("ALL TESTS PASSED\n"); + printf("========================================\n"); + + return g_tests_failed ? 1 : 0; +}