TODO.md section 2.1 recorded six defects reproduced against the built
library, and section 2.2 seventeen contract gaps. Both are closed. The
four tests registered in AKSL_KNOWN_FAILING_TESTS are folded back into
the tests for the things they test, and that list is now empty.
The defects:
2.1.1 aksl_list_append conflated Floyd cycle detection with finding
the tail, so `tail` tracked the node behind the midpoint. Any
append to a list of 2+ nodes silently dropped everything after
it. Two separate walks now: Floyd to prove the list is finite,
then a plain walk to the end.
2.1.2 aksl_list_iterate started visiting from Floyd's `slow` cursor,
so the whole first half of the list -- head included -- was
never passed to the callback. It starts at the head.
2.1.3 AKERR_ITERATOR_BREAK did not stop a tree traversal: the frame
that raised it handled it and returned success, so the parent
carried on into the sibling subtree. The recursion is split out
and propagates the break; only the public entry swallows it.
2.1.4 va_end now matches every va_start on every path.
2.1.5 The ato* family had no error channel at all. Reimplemented over
a new strto* family with errno cleared, an endptr check and a
range check: AKERR_VALUE for junk, ERANGE for overflow.
2.1.6 aksl_realpath never checked resolved_path, could not be told
the buffer size, and formatted an unspecified buffer with %s on
its own error path. It takes a length; aksl_realpath_alloc is
the allocating form.
The contract gaps, in brief: errno is cleared before every wrapped call
and read back through a fallback so no error can carry status 0; fopen
validates pathname and mode; fread/fwrite report the transferred count
through a required out-param and no longer call a short transfer a
success; aksl_sprintf is gone in favour of aksl_snprintf, which treats
truncation as an error; the variadic wrappers carry format attributes;
djb2 reads bytes as unsigned; tree traversal is depth- and cycle-bounded
and implements BFS, so lalloc/lfree are used rather than merely stored;
an unknown searchmode is AKERR_VALUE rather than silent success;
list_pop takes the head by reference; aksl_freep, the node initialisers
and extern "C" are new.
Build: -pg is out of the default build (it never reached the C compiler
anyway, and it is what produced the stray gmon.out), -Wall -Wextra are
in, and there is a .gitignore.
Tests: 11 binaries, all green under the normal and sanitizer builds.
Visit-order assertions replace the step counts that could not tell the
three depth-first orders apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
149 lines
4.5 KiB
C
149 lines
4.5 KiB
C
/*
|
|
* aksl_strhash_djb2 -- TODO.md section 1.6.
|
|
*
|
|
* The expected values are the canonical djb2 ones: h = 5381, then
|
|
* h = h * 33 + byte for each of len bytes, truncated to 32 bits. They were
|
|
* computed independently of this implementation.
|
|
*
|
|
* Most vectors here are 7-bit ASCII, where signed and unsigned char agree and
|
|
* the test therefore says nothing either way about byte signedness. The high-bit
|
|
* vector is the one that pins TODO.md 2.2.6 down.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
static int test_empty_string_is_the_djb2_seed(void)
|
|
{
|
|
uint32_t h = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2("", 0, &h));
|
|
AKSL_CHECK(h == 5381);
|
|
return 0;
|
|
}
|
|
|
|
/* len drives the loop, so a zero length ignores the contents entirely. */
|
|
static int test_zero_length_ignores_the_buffer(void)
|
|
{
|
|
char buf[] = "ignored";
|
|
uint32_t h = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 0, &h));
|
|
AKSL_CHECK(h == 5381);
|
|
return 0;
|
|
}
|
|
|
|
static int test_known_answer_vectors(void)
|
|
{
|
|
char hello[] = "hello";
|
|
char libname[] = "libakstdlib";
|
|
uint32_t h = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(hello, 5, &h));
|
|
AKSL_CHECK(h == 261238937u);
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(libname, 11, &h));
|
|
AKSL_CHECK(h == 884285482u);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* TODO.md 2.2.6, pinned. The cursor is an unsigned char * now, so bytes at or
|
|
* above 0x80 contribute their unsigned value. Iterating a plain char * on x86 or
|
|
* ARM Linux made them negative, giving 5859874 here instead of 5868578 -- a hash
|
|
* that disagreed with canonical djb2 and, worse, disagreed with itself across
|
|
* platforms depending on whether char happened to be signed.
|
|
*
|
|
* Benign for the 7-bit identifiers akbasic hashes; quietly wrong for the first
|
|
* caller to key a table on a filename or a UTF-8 string literal.
|
|
*/
|
|
static int test_high_bit_bytes_are_unsigned(void)
|
|
{
|
|
char buf[2] = { (char)0xff, (char)0xfe };
|
|
uint32_t h = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &h));
|
|
AKSL_CHECK(h == 5868578u);
|
|
/* The sign-extended answer, spelled out so a regression names itself. */
|
|
AKSL_CHECK(h != 5859874u);
|
|
return 0;
|
|
}
|
|
|
|
/* The NUL-terminated convenience form (TODO.md 3.6) agrees with the length one. */
|
|
static int test_str_form_matches_the_length_form(void)
|
|
{
|
|
const char *s = "libakstdlib";
|
|
uint32_t from_str = 0;
|
|
uint32_t from_len = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2_str(s, &from_str));
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(s, strlen(s), &from_len));
|
|
AKSL_CHECK(from_str == from_len);
|
|
AKSL_CHECK(from_str == 884285482u);
|
|
|
|
/* The empty string is the seed, not an error. */
|
|
AKSL_CHECK_OK(aksl_strhash_djb2_str("", &from_str));
|
|
AKSL_CHECK(from_str == 5381);
|
|
|
|
AKSL_CHECK_STATUS(aksl_strhash_djb2_str(NULL, &from_str), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strhash_djb2_str("x", NULL), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
/* The function is length-driven, not NUL-driven: an embedded NUL is hashed. */
|
|
static int test_embedded_nul_is_hashed(void)
|
|
{
|
|
char buf[3] = { 'a', '\0', 'b' };
|
|
uint32_t whole = 0;
|
|
uint32_t prefix = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf), &whole));
|
|
AKSL_CHECK(whole == 193482728u);
|
|
|
|
/* Stopping at the NUL would give the one-byte hash instead. */
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, 1, &prefix));
|
|
AKSL_CHECK(prefix != whole);
|
|
return 0;
|
|
}
|
|
|
|
static int test_hash_is_stable_across_calls(void)
|
|
{
|
|
char buf[] = "repeatable";
|
|
uint32_t first = 0;
|
|
uint32_t second = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &first));
|
|
AKSL_CHECK_OK(aksl_strhash_djb2(buf, sizeof(buf) - 1, &second));
|
|
AKSL_CHECK(first == second);
|
|
return 0;
|
|
}
|
|
|
|
static int test_rejects_null_arguments(void)
|
|
{
|
|
char buf[] = "x";
|
|
uint32_t h = 0;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(NULL, 1, &h),
|
|
AKERR_NULLPOINTER, "str");
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strhash_djb2(buf, 1, NULL),
|
|
AKERR_NULLPOINTER, "hashval");
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_empty_string_is_the_djb2_seed);
|
|
AKSL_RUN(failures, test_zero_length_ignores_the_buffer);
|
|
AKSL_RUN(failures, test_known_answer_vectors);
|
|
AKSL_RUN(failures, test_high_bit_bytes_are_unsigned);
|
|
AKSL_RUN(failures, test_str_form_matches_the_length_form);
|
|
AKSL_RUN(failures, test_embedded_nul_is_hashed);
|
|
AKSL_RUN(failures, test_hash_is_stable_across_calls);
|
|
AKSL_RUN(failures, test_rejects_null_arguments);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|