btk

Timeline
Login

Timeline

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

25 check-ins

2026-10-05
08:34
sb: refuse lengths that would overflow the buffer size

Port the SIZE_MAX guards from the bmaild tree so the growth arithmetic cannot wrap around on absurd inputs (sb_grow, sb_cat, sb_cat_n, sb_cat_c, sb_printf). The existing NULL-pointer tolerance is kept.

This makes btk strictly a superset of the copy bmaild carried, so bmaild can drop its duplicate and consume btk directly. leaf check-in: 74a7f376ca user: bapt tags: main, trunk

2026-10-02
17:34
xmalloc: let xstrdup()/xstrndup() accept NULL

strdup(NULL) reaches strlen(NULL) and crashes. Callers routinely pass values read from optional fields, headers or absent JSON entries, where a missing value is a normal case, so returning NULL is the only sane behaviour.

bmail's vendored copy of this header had the guard and its test suite asserts it, which is how the two implementations could be merged; note that our xstrndup(NULL, n) returns NULL too, so the n argument is ignored when str is NULL.

Cover both in xmalloc_test. check-in: 251a7d4f1d user: bapt tags: main, trunk

2026-09-21
16:11
strhash: drop endianness note from the comment check-in: 3900e40d63 user: bapt tags: main, trunk
16:11
hash: add hash_foreach() loop macro

Wrap hash_iterator()/hash_next() in a hash_foreach() macro that declares the loop variable and iterates over every entry, mirroring vec_foreach(). Add a test and register it. check-in: c59c9a1926 user: bapt tags: main, trunk

15:16
sb: always allocate in sb_grow for a NULL buffer

sb_grow() returned early whenever needed <= capacity, even when the buffer was still NULL. That relied on the implicit invariant that a NULL buffer always has capacity 0 and made the clang static analyzer flag a possible NULL dereference in sb_get()/sb_str(), which call sb_grow(sb, 1) and then write sb->d[0].

Also require the buffer to be non-NULL before skipping the allocation, so sb_grow() always leaves sb with a usable buffer. Test the needed == 0 case. check-in: 295a7c753a user: bapt tags: main, trunk

15:13
stringset: report whether stringset_safe_add added

hash_safe_add() returns a bool telling whether the entry was added, but stringset_safe_add() was still a do/while statement macro discarding stringset_add()'s result. Make it a statement expression yielding true when the key was newly added and false otherwise, matching its hash counterpart.

Statement usage keeps working; extend the test. check-in: 1581fc7252 user: bapt tags: main, trunk

15:05
Purge orphan test binaries and add a README

clean only removed the binaries of the tests currently listed in TESTS, so removing a test program left its binary behind (e.g. tests/mum_test). Use the tests/*_test glob instead.

Add a short README documenting the modules, how to build, and that make check needs kyua and the ATF C library (devel/atf) under LOCALBASE. check-in: 76a4d93522 user: bapt tags: main, trunk

15:03
hash, stringset: handle a NULL key as an absent key

The table pointer was guarded but not the key: hash_get(), hash_add(), hash_del(), hash_delete(), stringset_contains(), stringset_add() and stringset_del() passed the key straight to strhash(key, strlen(key)) and crashed on a NULL key.

Treat a NULL key like an absent one, consistently with the existing NULL table handling: lookups report not found, mutators are no-ops returning false. Add NULL-key tests. check-in: 6613b4c812 user: bapt tags: main, trunk

15:02
hash: make hash_safe_add() report whether it added

hash_safe_add() was a do/while statement macro that discarded hash_add()'s result, so a caller could not tell whether its value had been stored or rejected because the key already existed. Passing an inline xstrdup() leaked silently in the latter case.

Turn it into a statement expression yielding true when the entry was added and false otherwise (duplicate, allocation failure or NULL table). On a rejected add the value is left to the caller, so ownership can be handled explicitly:

if (!hash_safe_add(h, k, v, free)) free(v);

The redundant hash_get() pre-check is gone, hash_add() already refuses duplicates, and statement usage keeps working. Extend the test. check-in: efbe08a918 user: bapt tags: main, trunk

14:58
vec: make vec_first/vec_last safe on empty vectors

vec_first() dereferenced d[0] and vec_last() indexed d[(v)->len - 1], so on an empty vector the former could dereference a NULL data pointer and the latter underflowed the index to SIZE_MAX and read out of bounds.

Give them the same treatment as vec_pop(): a statement expression returning a zeroed element when the vector is empty. They stop being lvalues, which no caller relied on (mlmmj uses them as rvalues; pkg and rcd do not use them). Extend the empty-vector test. check-in: 009274eccd user: bapt tags: main, trunk

14:57
vec: document pop/pop_front return semantics

vec_pop() expands to a statement expression, so like vec_pop_front() it yields a value and is no longer usable as an lvalue (it used to be one before it gained its empty-vector guard). Document that, along with the zeroed element returned on an empty vector. check-in: 9839257bc5 user: bapt tags: main, trunk

14:57
hash: document hash_safe_add duplicate semantics

When the key is already present hash_safe_add() does nothing, so the value argument is neither stored nor freed by the macro. Spell that out so a caller does not pass an inline xstrdup() that would leak, and extend the pre-existing note about the macro evaluating its first argument several times. check-in: 2086933ce2 user: bapt tags: main, trunk

14:56
sb: compute strlen(s) after the NULL check

sb_cat() called strlen(s) before checking sb for NULL, so the guard did not avoid the work it was meant to. Move the check first and compute the length afterwards. check-in: d640fe8efc user: bapt tags: main, trunk

14:55
strview: evaluate the sv() argument only once

sv(s) expanded to { .ptr = (s), .len = strlen(s) }, evaluating its argument twice. With a side-effecting argument such as sv(p++) the pointer and the length were taken from different values and produced a bogus view (clang even warns -Wunsequenced).

Wrap the expansion in a statement expression that stores the argument in a temporary first, so it is evaluated exactly once. Add a test counting the evaluations. check-in: 46b76a1e1f user: bapt tags: main, trunk

14:55
stringset: use ascii arrows in the doc comment

The comment describing the entry states used U+2192 arrows; keep the source ASCII-only. check-in: eda16bb113 user: bapt tags: main, trunk

14:51
tests: cover vec_remove_and_free and vec_clear_and_free

Both were only exercised on an empty vector; add the non-empty cases and check the freed count and the resulting contents. check-in: 2bb788b93d user: bapt tags: main, trunk

14:50
Document hash_add() and the safe_add() macros

hash_add() returns false and keeps the existing value when the key is already present, with no replace variant (pkg's pkghash works the same way). Spell that out, along with the ownership rules of hash_del() and hash_delete(), and note that the safe_add() macros evaluate their first argument several times. check-in: 15c125d81a user: bapt tags: main, trunk

14:50
Make the public entry points uniformly NULL-safe

NULL handling was inconsistent: hash_count(), hash_destroy(), hash_get(), hash_del(), stringset_count(), stringset_destroy(), stringset_contains(), stringset_del(), sb_fini(), sb_reset() and sb_get() all accepted a NULL pointer, but hash_add(), stringset_add(), sb_str(), sb_cat(), sb_cat_n(), sb_cat_c(), sb_printf() and sb_grow() dereferenced it and crashed.

Guard the remaining entry points: the mutators become no-ops returning false, sb_str() returns NULL like sb_get() already does. Add NULL tests for sb, hash and stringset. check-in: 7081f1a1fe user: bapt tags: main, trunk

14:49
strview: tighten sv_to_int input validation

sv_to_int() accepted an empty view as the integer 0, and because strtol() skips leading whitespace it accepted " 10" while rejecting "10 " -- an inconsistency given sv_trim() exists for callers that want to strip padding.

Require a non-empty view whose first byte is a sign or a digit, so the parse is strict and symmetric: no empty input, no surrounding whitespace, no trailing garbage. This also makes the <ctype.h> include (through isdigit) actually used. Update the tests accordingly. check-in: f461417464 user: bapt tags: main, trunk

14:48
vec: make pop/remove safe on empty vectors

vec_pop(), vec_pop_front(), vec_remove() and vec_remove_and_free() had no precondition check: on an empty vector they read out of bounds or underflowed the index arithmetic (vec_remove() computed (v)->len - 1 as SIZE_MAX) and died with a random access violation.

vec_swap_remove() only guarded itself with assert(), which is compiled out under NDEBUG, so release builds had the same problem.

Guard the bodies so empty and out-of-range operations are defined no-ops, and make vec_pop()/vec_pop_front() return a zeroed element instead of dereferencing a NULL data pointer. This keeps the hot path to a single predictable branch and, unlike an assert, also protects NDEBUG builds.

Drop the now-unused <assert.h> include and add tests for the empty vector and for removing/popping the last element. check-in: 2d67d67504 user: bapt tags: main, trunk

14:41
strview: reject out-of-range ints in sv_to_int

sv_to_int() fed strtol()'s long result straight into an int with no range check, so values outside int silently truncated: "2147483648" returned success with INT_MIN and "99999999999999" returned garbage. strtol() overflow (ERANGE) was ignored too.

Set errno before the call, reject ERANGE, and reject values outside INT_MIN..INT_MAX. The boundaries themselves are still accepted. Extend the tests with the boundary and overflow cases. check-in: d604b70722 user: bapt tags: main, trunk

14:41
vec: include string.h for the memset in vec_free

vec_free(), vec_free_and_free() and vec_clear_and_free() expand to a memset() call but vec.h never included <string.h>. A translation unit that includes vec.h without string.h first fails to compile under C99 or with -Werror; the tests did not catch it because they include <string.h> beforehand. check-in: 9b7cc126eb user: bapt tags: main, trunk

14:30
Replace mum.h with a word-at-a-time strhash

btk only ever needed a 64-bit hash of a key from a string. mum.h is 400 lines with 122 preprocessor branches, architecture-specific asm, and a randomize path that was never used (the seed was always 0), so its main selling point was dead weight here.

Replace it with strhash.h: a word-at-a-time multiply-xor with a splitmix64 finalizer, which provides the low-bit avalanche the tables rely on for their index (& (capacity - 1)). It needs nothing beyond memcpy.

Benchmarks modelling the real pkg and rcd workloads (plist paths, package uids, rc variables; keys well under MAXPATHLEN) show no measurable difference: the hash is about 1 percent of the insert cost, which is dominated by strdup/malloc.

Drop mum.h and the mum-specific test, add strhash_test covering determinism, length sensitivity and collision-freedom on short keys. check-in: 88c925f85a user: bapt tags: main, trunk

14:30
hash: rehash on tombstone buildup

hash_del() and hash_delete() mark entries as tombstones, but hash_add() only rehashed based on the live count. During add/delete churn on the same keys the count stays low while tombstones accumulate; once every slot is a tombstone the probe loops in hash_set_entry() and hash_get() never find a free slot and spin forever.

Track the number of tombstones and rehash when they exceed capacity/4, mirroring what stringset.c already does. Reset the counter on expand.

Add a churn regression test that would hang without the fix. check-in: db2f184219 user: bapt tags: main, trunk

13:49
Add btk C utility toolkit with test suite

Small collection of reusable C helpers plus an ATF/kyua test suite:

- sb: string buffer (sbuf-like growth) - hash: open-addressing hash table - stringset: open-addressing string set - vec, strview, xmalloc, mum: header-only utilities

Also fix a double free in hash_del()/hash_delete(): the stale value pointer and free_func were left in the entry, so hash_destroy() freed the value a second time. Clear both when the value is released or its ownership transferred to the caller. check-in: 896842b1ed user: bapt tags: main, trunk