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>
200 lines
6.6 KiB
C
200 lines
6.6 KiB
C
/*
|
|
* aksl_realpath and aksl_realpath_alloc -- TODO.md section 1.5, now complete.
|
|
*
|
|
* The happy paths compare against realpath(3) itself rather than against a
|
|
* hard-coded string, because $TMPDIR may itself be a symlink (/tmp -> /private/tmp
|
|
* and friends) and the resolved answer is what the platform says it is.
|
|
*
|
|
* The failure cases now pass an *uninitialised* resolved_path on purpose. That
|
|
* used to be the crash case (TODO.md 2.1.6): the wrapper's own error path
|
|
* formatted the buffer with %s while realpath(3) leaves its contents
|
|
* unspecified on failure, so the library read uninitialised memory while
|
|
* reporting an error. The message names only the input path now, and this test
|
|
* is what holds that -- it is meant to be run under the sanitizer build, where a
|
|
* regression is an immediate abort rather than a silent read of stack garbage.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
|
|
static int test_resolves_an_existing_file(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char resolved[PATH_MAX];
|
|
char expected[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
memset(resolved, 0x00, sizeof(resolved));
|
|
AKSL_CHECK(realpath(path, expected) != NULL);
|
|
|
|
AKSL_CHECK_OK(aksl_realpath(path, resolved, sizeof(resolved)));
|
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
|
AKSL_CHECK(resolved[0] == '/');
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* A symlink resolves to its target, not to itself. */
|
|
static int test_resolves_a_symlink_to_its_target(void)
|
|
{
|
|
char target[AKSL_TMP_MAX];
|
|
char link[AKSL_TMP_MAX];
|
|
char resolved[PATH_MAX];
|
|
char expected[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(target, sizeof(target)) == 0);
|
|
AKSL_CHECK(aksl_temp_file(link, sizeof(link)) == 0);
|
|
/* mkstemp created the link path as a regular file; symlink needs it gone. */
|
|
AKSL_CHECK(unlink(link) == 0);
|
|
AKSL_CHECK(symlink(target, link) == 0);
|
|
|
|
memset(resolved, 0x00, sizeof(resolved));
|
|
AKSL_CHECK(realpath(target, expected) != NULL);
|
|
AKSL_CHECK_OK(aksl_realpath(link, resolved, sizeof(resolved)));
|
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
|
|
|
AKSL_CHECK(unlink(link) == 0);
|
|
AKSL_CHECK(unlink(target) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The failure path with a buffer nobody has written to. Uninitialised on
|
|
* purpose: see the header comment. Under ASan/MSan this is the test that fails
|
|
* if the error path ever starts reading resolved_path again.
|
|
*/
|
|
static int test_missing_path_reports_enoent(void)
|
|
{
|
|
char resolved[PATH_MAX];
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
|
aksl_realpath("/nonexistent/aksl/path", resolved, sizeof(resolved)),
|
|
ENOENT, "/nonexistent/aksl/path");
|
|
/* The message must name the path and must not quote the buffer back. */
|
|
AKSL_CHECK(strstr(aksl_last_message, "resolved") == NULL);
|
|
return 0;
|
|
}
|
|
|
|
/* A regular file used as a directory component is ENOTDIR, not ENOENT. */
|
|
static int test_non_directory_component_reports_enotdir(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char child[AKSL_TMP_MAX + 8];
|
|
char resolved[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK((size_t)snprintf(child, sizeof(child), "%s/child", path)
|
|
< sizeof(child));
|
|
|
|
AKSL_CHECK_STATUS(aksl_realpath(child, resolved, sizeof(resolved)), ENOTDIR);
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* Two symlinks pointing at each other: the kernel gives up with ELOOP. */
|
|
static int test_symlink_loop_reports_eloop(void)
|
|
{
|
|
char a[AKSL_TMP_MAX];
|
|
char b[AKSL_TMP_MAX];
|
|
char resolved[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(a, sizeof(a)) == 0);
|
|
AKSL_CHECK(aksl_temp_file(b, sizeof(b)) == 0);
|
|
AKSL_CHECK(unlink(a) == 0);
|
|
AKSL_CHECK(unlink(b) == 0);
|
|
AKSL_CHECK(symlink(a, b) == 0);
|
|
AKSL_CHECK(symlink(b, a) == 0);
|
|
|
|
AKSL_CHECK_STATUS(aksl_realpath(a, resolved, sizeof(resolved)), ELOOP);
|
|
|
|
AKSL_CHECK(unlink(a) == 0);
|
|
AKSL_CHECK(unlink(b) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_rejects_null_arguments(void)
|
|
{
|
|
char resolved[PATH_MAX];
|
|
|
|
memset(resolved, 0x00, sizeof(resolved));
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
|
|
AKERR_NULLPOINTER, "path=");
|
|
/*
|
|
* TODO.md 2.1.6: this used to be unchecked, and realpath(path, NULL)
|
|
* allocated a buffer that the wrapper then discarded and leaked.
|
|
*/
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX),
|
|
AKERR_NULLPOINTER, "resolved_path=");
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* realpath(3) cannot be told how much room it has, so the only safe answer to an
|
|
* undersized buffer is to refuse before calling it. A caller who gets this back
|
|
* has a bug that would otherwise have been a stack smash.
|
|
*/
|
|
static int test_rejects_a_buffer_below_path_max(void)
|
|
{
|
|
char small[16];
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", small, sizeof(small)),
|
|
AKERR_OUTOFBOUNDS, "PATH_MAX");
|
|
return 0;
|
|
}
|
|
|
|
static int test_alloc_form_resolves_and_hands_over_the_buffer(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char expected[PATH_MAX];
|
|
char *resolved = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK(realpath(path, expected) != NULL);
|
|
|
|
AKSL_CHECK_OK(aksl_realpath_alloc(path, &resolved));
|
|
AKSL_CHECK(resolved != NULL);
|
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
|
/* The buffer is the caller's; releasing it through the library closes the
|
|
* leak that the old NULL-destination path opened. */
|
|
AKSL_CHECK_OK(aksl_free(resolved));
|
|
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_alloc_form_reports_failure_and_writes_no_pointer(void)
|
|
{
|
|
char *resolved = (char *)0x1;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
|
aksl_realpath_alloc("/nonexistent/aksl/path", &resolved),
|
|
ENOENT, "/nonexistent/aksl/path");
|
|
/* Cleared before the call, so a failure cannot leave a stale pointer. */
|
|
AKSL_CHECK(resolved == NULL);
|
|
|
|
AKSL_CHECK_STATUS(aksl_realpath_alloc(NULL, &resolved), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_realpath_alloc("/tmp", NULL), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_resolves_an_existing_file);
|
|
AKSL_RUN(failures, test_resolves_a_symlink_to_its_target);
|
|
AKSL_RUN(failures, test_missing_path_reports_enoent);
|
|
AKSL_RUN(failures, test_non_directory_component_reports_enotdir);
|
|
AKSL_RUN(failures, test_symlink_loop_reports_eloop);
|
|
AKSL_RUN(failures, test_rejects_null_arguments);
|
|
AKSL_RUN(failures, test_rejects_a_buffer_below_path_max);
|
|
AKSL_RUN(failures, test_alloc_form_resolves_and_hands_over_the_buffer);
|
|
AKSL_RUN(failures, test_alloc_form_reports_failure_and_writes_no_pointer);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|