All checks were successful
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a numbering the file had already abandoned before the move to the tracker. They label completed work, so the pointer was the only wrong part. The citation is removed and the sentence kept, which is what issue #27 recommended: these are labels, not references, and a label carrying a version-dependent pointer goes stale again at the next reorganisation. Where a pointer earns its place it names what actually holds the content now -- UPGRADING.md for the confirmed defects, libakerror #15 for the target namespacing, issue #7 for the mutation survivors. README.md and akstdlib.h sent readers to TODO.md for 'what is still open'; they name the tracker. Verified: cmake --build build && ctest --test-dir build, 19/19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
331 lines
11 KiB
C
331 lines
11 KiB
C
/*
|
|
* Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose.
|
|
*
|
|
* Stream wrappers, complete. The happy paths, the round trip, every
|
|
* NULL guard, both stream-error statuses, the transferred-member count that
|
|
* aksl_fread and aksl_fwrite report through nmemb_out, and the three cases that
|
|
* needed a hostile file to produce: a mode-denied path (EACCES), a full device
|
|
* (/dev/full) for the short write, and a stream whose buffered flush fails at
|
|
* fclose time.
|
|
*
|
|
* The /dev/full tests are skipped where the device is absent -- it is a Linux
|
|
* thing -- rather than failing, and say so on stderr so a skip cannot be
|
|
* mistaken for a pass.
|
|
*
|
|
* Temp files come from aksl_temp_file() and are unlinked by the test that made
|
|
* them, so a failing test leaves nothing behind but the file it was mid-way
|
|
* through.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
#include <errno.h>
|
|
#include <sys/stat.h>
|
|
|
|
static int test_fopen_writes_stream_pointer(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
FILE *fp = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
|
AKSL_CHECK(fp != NULL);
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_fopen_reports_missing_path(void)
|
|
{
|
|
FILE *fp = NULL;
|
|
|
|
/* The pathname belongs in the message; the status is the errno fopen saw. */
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
|
aksl_fopen("/nonexistent/aksl/stream", "r", &fp),
|
|
ENOENT, "/nonexistent/aksl/stream");
|
|
/* *fp is cleared before the call, so a failed open cannot leave garbage. */
|
|
AKSL_CHECK(fp == NULL);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* A file the caller cannot open for reading is EACCES rather than ENOENT.
|
|
* Skipped under a user who bypasses the permission bits -- root, or anything
|
|
* holding CAP_DAC_OVERRIDE -- where chmod 000 simply does not deny anything.
|
|
*/
|
|
static int test_fopen_reports_permission_denied(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
FILE *fp = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK(chmod(path, 0000) == 0);
|
|
|
|
if ( geteuid() == 0 ) {
|
|
fprintf(stderr, " (skipped: running as root, chmod 000 denies nothing)\n");
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", &fp), EACCES, path);
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_fopen_rejects_null_arguments(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
FILE *fp = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, "r", NULL),
|
|
AKERR_NULLPOINTER, "fp=");
|
|
/* both of these used to go straight through to fopen(3). */
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(NULL, "r", &fp),
|
|
AKERR_NULLPOINTER, "pathname=");
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fopen(path, NULL, &fp),
|
|
AKERR_NULLPOINTER, "mode=");
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* fopen -> fwrite -> fclose -> fopen -> fread -> compare, all through the wrappers. */
|
|
static int test_write_read_round_trip(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char payload[] = "libakstdlib round trip";
|
|
char readback[sizeof(payload)];
|
|
size_t moved = 0;
|
|
FILE *fp = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
|
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
|
AKSL_CHECK_OK(aksl_fwrite(payload, 1, sizeof(payload), fp, &moved));
|
|
AKSL_CHECK(moved == sizeof(payload));
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
|
|
fp = NULL;
|
|
moved = 0;
|
|
memset(readback, 0x00, sizeof(readback));
|
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
|
AKSL_CHECK_OK(aksl_fread(readback, 1, sizeof(readback), fp, &moved));
|
|
AKSL_CHECK(moved == sizeof(readback));
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
|
|
AKSL_CHECK(memcmp(payload, readback, sizeof(payload)) == 0);
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Asking for more members than the file holds sets feof, which is AKERR_EOF --
|
|
* and nmemb_out says how many did arrive, which is the entire reason for
|
|
* distinguishing EOF from an error rather than reporting both as AKERR_IO.
|
|
*/
|
|
static int test_fread_short_read_is_eof_and_reports_the_count(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char buf[32];
|
|
size_t moved = 0;
|
|
FILE *fp = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
|
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
|
AKSL_CHECK_OK(aksl_fwrite("abcd", 1, 4, fp, &moved));
|
|
AKSL_CHECK(moved == 4);
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
|
|
fp = NULL;
|
|
moved = 99;
|
|
memset(buf, 0x00, sizeof(buf));
|
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
|
|
AKERR_EOF, "EOF");
|
|
/* this is what the caller could not previously find out. */
|
|
AKSL_CHECK(moved == 4);
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
|
|
AKSL_CHECK(memcmp(buf, "abcd", 4) == 0);
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* A stream opened "w" has no read permission, so fread sets the error indicator
|
|
* rather than the EOF one -- and the status is the errno the C library actually
|
|
* saw, EBADF, rather than a flat AKERR_IO.
|
|
*
|
|
* That is a change from the old wrapper, which read ferror() and reported
|
|
* AKERR_IO without ever consulting errno. Routing it through AKSL_ERRNO_OR
|
|
* keeps AKERR_IO as the fallback for the case where the stream
|
|
* is in error and errno says nothing, and hands back the real reason otherwise.
|
|
*/
|
|
static int test_fread_from_write_only_stream_reports_errno(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char buf[4];
|
|
size_t moved = 99;
|
|
FILE *fp = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK_OK(aksl_fopen(path, "w", &fp));
|
|
memset(buf, 0x00, sizeof(buf));
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, &moved),
|
|
EBADF, "of 4 members");
|
|
AKSL_CHECK(moved == 0);
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_fread_rejects_null_arguments(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char buf[4];
|
|
size_t moved = 0;
|
|
FILE *fp = NULL;
|
|
|
|
memset(buf, 0x00, sizeof(buf));
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), NULL, &moved),
|
|
AKERR_NULLPOINTER, "fp=");
|
|
/* ptr was never checked in either direction. */
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(NULL, 1, sizeof(buf), fp, &moved),
|
|
AKERR_NULLPOINTER, "ptr=");
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fread(buf, 1, sizeof(buf), fp, NULL),
|
|
AKERR_NULLPOINTER, "nmemb_out=");
|
|
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* Mirror image of the fread case: a "r" stream cannot be written to. */
|
|
static int test_fwrite_to_read_only_stream_reports_errno(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
size_t moved = 99;
|
|
FILE *fp = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK_OK(aksl_fopen(path, "r", &fp));
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, fp, &moved),
|
|
EBADF, "wrote 0 of 2 members");
|
|
AKSL_CHECK(moved == 0);
|
|
AKSL_CHECK_OK(aksl_fclose(fp));
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_fwrite_rejects_null_arguments(void)
|
|
{
|
|
size_t moved = 0;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, NULL, &moved),
|
|
AKERR_NULLPOINTER, "fp=");
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite(NULL, 1, 2, stdout, &moved),
|
|
AKERR_NULLPOINTER, "ptr=");
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fwrite("xy", 1, 2, stdout, NULL),
|
|
AKERR_NULLPOINTER, "nmemb_out=");
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* /dev/full accepts an open for writing and then fails every write with ENOSPC.
|
|
* It is the only convenient way to reach a genuinely short write on a stream
|
|
* that is otherwise perfectly healthy.
|
|
*/
|
|
static int test_fwrite_to_full_device_reports_enospc(void)
|
|
{
|
|
char payload[8192];
|
|
size_t moved = 99;
|
|
FILE *fp = NULL;
|
|
|
|
if ( access("/dev/full", W_OK) != 0 ) {
|
|
fprintf(stderr, " (skipped: no writable /dev/full on this platform)\n");
|
|
return 0;
|
|
}
|
|
|
|
memset(payload, 'x', sizeof(payload));
|
|
AKSL_CHECK_OK(aksl_fopen("/dev/full", "w", &fp));
|
|
/*
|
|
* Larger than any plausible stdio buffer, so the write reaches the device
|
|
* inside fwrite rather than being deferred to fclose -- the deferred case is
|
|
* the next test.
|
|
*/
|
|
AKSL_CHECK_STATUS(aksl_fwrite(payload, 1, sizeof(payload), fp, &moved), ENOSPC);
|
|
AKSL_CHECK(moved < sizeof(payload));
|
|
/*
|
|
* fclose succeeds here, and that is not a contradiction: the write already
|
|
* reached the device and failed, so by close time there is nothing left
|
|
* buffered to fail on. The deferred case -- where the write fits in the
|
|
* stdio buffer and only fclose finds out -- is the next test. Take the
|
|
* context however it comes back rather than asserting either way, since
|
|
* whether the error indicator survives to fclose is a stdio implementation
|
|
* detail and not part of this library's contract.
|
|
*/
|
|
(void)aksl_take(aksl_fclose(fp));
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The write that fits in the stdio buffer succeeds, and the failure surfaces
|
|
* only when fclose flushes it. A caller who checks fwrite and ignores fclose
|
|
* loses the data silently, which is why aksl_fclose reports errno rather than
|
|
* just a non-zero return.
|
|
*/
|
|
static int test_fclose_surfaces_a_failed_flush(void)
|
|
{
|
|
size_t moved = 0;
|
|
FILE *fp = NULL;
|
|
|
|
if ( access("/dev/full", W_OK) != 0 ) {
|
|
fprintf(stderr, " (skipped: no writable /dev/full on this platform)\n");
|
|
return 0;
|
|
}
|
|
|
|
AKSL_CHECK_OK(aksl_fopen("/dev/full", "w", &fp));
|
|
AKSL_CHECK_OK(aksl_fwrite("small", 1, 5, fp, &moved));
|
|
AKSL_CHECK(moved == 5);
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fclose(fp), ENOSPC, "buffered data is lost");
|
|
return 0;
|
|
}
|
|
|
|
static int test_fclose_rejects_null_stream(void)
|
|
{
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_fclose(NULL),
|
|
AKERR_NULLPOINTER, "NULL");
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_fopen_writes_stream_pointer);
|
|
AKSL_RUN(failures, test_fopen_reports_missing_path);
|
|
AKSL_RUN(failures, test_fopen_reports_permission_denied);
|
|
AKSL_RUN(failures, test_fopen_rejects_null_arguments);
|
|
|
|
AKSL_RUN(failures, test_write_read_round_trip);
|
|
AKSL_RUN(failures, test_fread_short_read_is_eof_and_reports_the_count);
|
|
AKSL_RUN(failures, test_fread_from_write_only_stream_reports_errno);
|
|
AKSL_RUN(failures, test_fread_rejects_null_arguments);
|
|
|
|
AKSL_RUN(failures, test_fwrite_to_read_only_stream_reports_errno);
|
|
AKSL_RUN(failures, test_fwrite_rejects_null_arguments);
|
|
AKSL_RUN(failures, test_fwrite_to_full_device_reports_enospc);
|
|
|
|
AKSL_RUN(failures, test_fclose_surfaces_a_failed_flush);
|
|
AKSL_RUN(failures, test_fclose_rejects_null_stream);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|