/* * Stream wrappers: aksl_fopen / aksl_fread / aksl_fwrite / aksl_fclose. * * TODO.md section 1.2, now 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 #include 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="); /* TODO.md 2.2.2: 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"); /* TODO.md 2.2.3: 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 * (TODO.md 2.2.1) 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="); /* TODO.md 2.2.3: 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); }