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>
293 lines
10 KiB
C
293 lines
10 KiB
C
/*
|
|
* The strto* family -- TODO.md section 3.1.
|
|
*
|
|
* These are the real implementation behind the ato* wrappers and the thing
|
|
* akbasic had to hand-write for itself (its src/convert.c, ~60 lines) because
|
|
* the library would not do it. What they add over the ato* forms is a base and
|
|
* an endptr: pass a non-NULL endptr to parse a number off the front of a longer
|
|
* string and find out where it stopped, and the trailing-junk check goes away
|
|
* because the caller has taken responsibility for what follows.
|
|
*
|
|
* tests/test_convert.c covers what the two families share; this file is the part
|
|
* that is specific to having a base and an endptr.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Base handling */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_base_is_honoured(void)
|
|
{
|
|
long out = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strtol("ff", NULL, 16, &out));
|
|
AKSL_CHECK(out == 255);
|
|
AKSL_CHECK_OK(aksl_strtol("1010", NULL, 2, &out));
|
|
AKSL_CHECK(out == 10);
|
|
AKSL_CHECK_OK(aksl_strtol("777", NULL, 8, &out));
|
|
AKSL_CHECK(out == 511);
|
|
AKSL_CHECK_OK(aksl_strtol("z", NULL, 36, &out));
|
|
AKSL_CHECK(out == 35);
|
|
return 0;
|
|
}
|
|
|
|
/* Base 0 auto-detects the 0x and 0 prefixes, which is the counterpart to
|
|
* tests/test_convert.c's test that aksl_atoi refuses "0x10". */
|
|
static int test_base_zero_detects_the_prefix(void)
|
|
{
|
|
long out = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strtol("0x10", NULL, 0, &out));
|
|
AKSL_CHECK(out == 16);
|
|
AKSL_CHECK_OK(aksl_strtol("010", NULL, 0, &out));
|
|
AKSL_CHECK(out == 8);
|
|
AKSL_CHECK_OK(aksl_strtol("10", NULL, 0, &out));
|
|
AKSL_CHECK(out == 10);
|
|
return 0;
|
|
}
|
|
|
|
/* A digit that does not belong to the base is where the number ends. */
|
|
static int test_digits_outside_the_base_end_the_number(void)
|
|
{
|
|
long out = 0;
|
|
char *end = NULL;
|
|
|
|
/* "9" is not an octal digit, so with a NULL endptr it is trailing junk. */
|
|
AKSL_CHECK_STATUS(aksl_strtol("129", NULL, 8, &out), AKERR_VALUE);
|
|
/* With an endptr the caller gets the partial parse and the stopping point. */
|
|
AKSL_CHECK_OK(aksl_strtol("129", &end, 8, &out));
|
|
AKSL_CHECK(out == 10); /* 012 octal */
|
|
AKSL_CHECK(end != NULL && *end == '9');
|
|
return 0;
|
|
}
|
|
|
|
/* An invalid base is EINVAL from strtol(3) rather than a silent answer. */
|
|
static int test_invalid_base_is_rejected(void)
|
|
{
|
|
long out = 99;
|
|
|
|
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 37, &out), EINVAL);
|
|
AKSL_CHECK(out == 0);
|
|
AKSL_CHECK_STATUS(aksl_strtol("10", NULL, 1, &out), EINVAL);
|
|
AKSL_CHECK(out == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* endptr */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/*
|
|
* With an endptr, trailing text is the caller's business and not an error. This
|
|
* is the shape a tokenizer wants: parse a number, carry on from where it ended.
|
|
*/
|
|
static int test_endptr_reports_where_parsing_stopped(void)
|
|
{
|
|
const char *input = "42 rest of the line";
|
|
char *end = NULL;
|
|
long out = 0;
|
|
|
|
AKSL_CHECK_OK(aksl_strtol(input, &end, 10, &out));
|
|
AKSL_CHECK(out == 42);
|
|
AKSL_CHECK(end == input + 2);
|
|
AKSL_CHECK(strcmp(end, " rest of the line") == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* Consuming nothing is still an error, endptr or not: there was no number. */
|
|
static int test_no_digits_is_an_error_even_with_an_endptr(void)
|
|
{
|
|
char *end = (char *)0x1;
|
|
long out = 99;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtol("nope", &end, 10, &out),
|
|
AKERR_VALUE, "no digits");
|
|
AKSL_CHECK(out == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* Successive calls chained through endptr walk a whole list of numbers. */
|
|
static int test_endptr_chains_across_a_list(void)
|
|
{
|
|
const char *input = "1,2,3";
|
|
char *cursor = (char *)input;
|
|
long out = 0;
|
|
long total = 0;
|
|
int seen = 0;
|
|
|
|
while ( *cursor != '\0' ) {
|
|
AKSL_CHECK_OK(aksl_strtol(cursor, &cursor, 10, &out));
|
|
total += out;
|
|
seen += 1;
|
|
if ( *cursor == ',' ) {
|
|
cursor++;
|
|
}
|
|
}
|
|
AKSL_CHECK(seen == 3);
|
|
AKSL_CHECK(total == 6);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Ranges and signs */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_range_errors_per_type(void)
|
|
{
|
|
long lout = 99;
|
|
long long llout = 99;
|
|
unsigned long ulout = 99;
|
|
unsigned long long ullout = 99;
|
|
char buf[64];
|
|
|
|
snprintf(buf, sizeof(buf), "%lld9", (long long)LLONG_MAX);
|
|
AKSL_CHECK_STATUS(aksl_strtol(buf, NULL, 10, &lout), ERANGE);
|
|
AKSL_CHECK(lout == 0);
|
|
AKSL_CHECK_STATUS(aksl_strtoll(buf, NULL, 10, &llout), ERANGE);
|
|
AKSL_CHECK(llout == 0);
|
|
|
|
snprintf(buf, sizeof(buf), "%llu9", (unsigned long long)ULLONG_MAX);
|
|
AKSL_CHECK_STATUS(aksl_strtoul(buf, NULL, 10, &ulout), ERANGE);
|
|
AKSL_CHECK(ulout == 0);
|
|
AKSL_CHECK_STATUS(aksl_strtoull(buf, NULL, 10, &ullout), ERANGE);
|
|
AKSL_CHECK(ullout == 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* strtoul(3) and strtoull(3) accept a leading '-' and hand back the negation
|
|
* wrapped into the unsigned range, so "-1" parses as ULONG_MAX and reports
|
|
* nothing at all. That is exactly the silent failure this library exists to
|
|
* surface, so the sign is refused before the call.
|
|
*/
|
|
static int test_unsigned_forms_refuse_a_negative(void)
|
|
{
|
|
unsigned long ulout = 99;
|
|
unsigned long long ullout = 99;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_strtoul("-1", NULL, 10, &ulout),
|
|
AKERR_VALUE, "negative");
|
|
AKSL_CHECK(ulout == 0);
|
|
/* Including behind leading whitespace, which strtoul skips. */
|
|
AKSL_CHECK_STATUS(aksl_strtoul(" -1", NULL, 10, &ulout), AKERR_VALUE);
|
|
AKSL_CHECK_STATUS(aksl_strtoull("-1", NULL, 10, &ullout), AKERR_VALUE);
|
|
AKSL_CHECK(ullout == 0);
|
|
|
|
/* A leading '+' is fine: it is a sign, not a negation. */
|
|
AKSL_CHECK_OK(aksl_strtoul("+7", NULL, 10, &ulout));
|
|
AKSL_CHECK(ulout == 7);
|
|
return 0;
|
|
}
|
|
|
|
static int test_unsigned_boundaries_round_trip(void)
|
|
{
|
|
unsigned long ulout = 0;
|
|
unsigned long long ullout = 0;
|
|
char buf[64];
|
|
|
|
snprintf(buf, sizeof(buf), "%lu", ULONG_MAX);
|
|
AKSL_CHECK_OK(aksl_strtoul(buf, NULL, 10, &ulout));
|
|
AKSL_CHECK(ulout == ULONG_MAX);
|
|
|
|
snprintf(buf, sizeof(buf), "%llu", ULLONG_MAX);
|
|
AKSL_CHECK_OK(aksl_strtoull(buf, NULL, 10, &ullout));
|
|
AKSL_CHECK(ullout == ULLONG_MAX);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Floating point */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_floating_point_forms(void)
|
|
{
|
|
double dout = 0.0;
|
|
float fout = 0.0f;
|
|
long double ldout = 0.0L;
|
|
char *end = NULL;
|
|
|
|
AKSL_CHECK_OK(aksl_strtod("2.5", NULL, &dout));
|
|
AKSL_CHECK(dout == 2.5);
|
|
AKSL_CHECK_OK(aksl_strtof("2.5", NULL, &fout));
|
|
AKSL_CHECK(fout == 2.5f);
|
|
AKSL_CHECK_OK(aksl_strtold("2.5", NULL, &ldout));
|
|
AKSL_CHECK(ldout == 2.5L);
|
|
|
|
AKSL_CHECK_OK(aksl_strtod("2.5rest", &end, &dout));
|
|
AKSL_CHECK(dout == 2.5);
|
|
AKSL_CHECK(strcmp(end, "rest") == 0);
|
|
|
|
AKSL_CHECK_STATUS(aksl_strtod("2.5rest", NULL, &dout), AKERR_VALUE);
|
|
AKSL_CHECK_STATUS(aksl_strtof("nope", NULL, &fout), AKERR_VALUE);
|
|
AKSL_CHECK_STATUS(aksl_strtold("nope", NULL, &ldout), AKERR_VALUE);
|
|
|
|
/* float has a much smaller range than double, and says so. */
|
|
AKSL_CHECK_STATUS(aksl_strtof("1e300", NULL, &fout), ERANGE);
|
|
AKSL_CHECK(fout == 0.0f);
|
|
AKSL_CHECK_OK(aksl_strtod("1e300", NULL, &dout));
|
|
AKSL_CHECK(dout > 0.0);
|
|
return 0;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* Argument validation */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
static int test_every_form_rejects_null_arguments(void)
|
|
{
|
|
long lout = 0;
|
|
long long llout = 0;
|
|
unsigned long ulout = 0;
|
|
unsigned long long ullout = 0;
|
|
double dout = 0.0;
|
|
float fout = 0.0f;
|
|
long double ldout = 0.0L;
|
|
|
|
AKSL_CHECK_STATUS(aksl_strtol(NULL, NULL, 10, &lout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtol("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoll(NULL, NULL, 10, &llout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoll("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoul(NULL, NULL, 10, &ulout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoul("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoull(NULL, NULL, 10, &ullout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtoull("1", NULL, 10, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtod(NULL, NULL, &dout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtod("1", NULL, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtof(NULL, NULL, &fout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtof("1", NULL, NULL), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtold(NULL, NULL, &ldout), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_strtold("1", NULL, NULL), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_base_is_honoured);
|
|
AKSL_RUN(failures, test_base_zero_detects_the_prefix);
|
|
AKSL_RUN(failures, test_digits_outside_the_base_end_the_number);
|
|
AKSL_RUN(failures, test_invalid_base_is_rejected);
|
|
|
|
AKSL_RUN(failures, test_endptr_reports_where_parsing_stopped);
|
|
AKSL_RUN(failures, test_no_digits_is_an_error_even_with_an_endptr);
|
|
AKSL_RUN(failures, test_endptr_chains_across_a_list);
|
|
|
|
AKSL_RUN(failures, test_range_errors_per_type);
|
|
AKSL_RUN(failures, test_unsigned_forms_refuse_a_negative);
|
|
AKSL_RUN(failures, test_unsigned_boundaries_round_trip);
|
|
|
|
AKSL_RUN(failures, test_floating_point_forms);
|
|
|
|
AKSL_RUN(failures, test_every_form_rejects_null_arguments);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|