Files
libakstdlib/src/stream.c
Andrew Kesterson 125eeb2109 Version at 0.2.0: complete the wishlist, document it, gate the docs
Closes what was left of TODO.md sections 1, 2 and 3, and rewrites that
file to hold outstanding items only.

The API break gets a minor bump, because pre-1.0 the soname carries
MAJOR.MINOR and 0.1 and 0.2 are therefore different ABIs. Five
signatures changed and the ato* contract with them; UPGRADING.md is new
and lists every one, with the before/after for the cases the compiler
cannot warn about.

Section 3.1 is finished: reallocarray with the multiplication checked,
aligned_alloc and posix_memalign, asprintf/vasprintf, scanf/vscanf.
Four functions on that list are deliberately absent rather than missing
-- sprintf, strtok, setbuf and perror -- and TODO.md now says which and
why, so nobody adds them thinking they were forgotten.

Section 1.9, the cross-cutting tests:

  tests/test_pool.c    drives every failure path AKERR_MAX_ARRAY_ERROR
                       + 10 times and checks the pool after each round,
                       because a wrapper that leaks a slot fails a
                       hundred calls later in unrelated code. It also
                       asserts that each error names the function and
                       file it was raised from, which is what catches a
                       FAIL that migrates into a helper during a
                       refactor: status right, message right, origin
                       quietly lying.
  tests/negative/      two sources that must FAIL to compile, built with
                       -Werror and registered WILL_FAIL. AKERR_NOIGNORE
                       and the format attributes are enforced by the
                       compiler and by nothing else; drop either and
                       every ordinary test still passes.

Thread safety is answered rather than tested: the library is not
thread-safe and cannot be made so from here, because libakerror's error
pool is an unlocked process-global array. README.md says so plainly and
TODO.md carries it as the item blocking any future pthread wrappers.

Doxygen is configured and gated. All 147 public functions have @brief,
a @param each, @throws per status and @return; EXTRACT_ALL is off and
WARN_NO_PARAMDOC on, so `cmake --build build --target docs` fails on an
undocumented entity. It ran to 0 warnings. The Doxyfile carries no
version -- cmake/RunDoxygen.cmake feeds PROJECT_NUMBER in from
project(), so that stays the one place a version is written.

CI now builds against the submodule it pins instead of also installing
libakerror@main and never linking it, adds -Werror, and gains a
sanitizer job. The pre-push hook matches, and runs the docs check too.

Coverage: 99.5% of lines (1643/1651), 100% of functions (147/147). The
eight uncovered lines are each uncovered on purpose and TODO.md says
which and why.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:00:16 -04:00

571 lines
23 KiB
C

/*
* stdio.h wrappers beyond fopen/fread/fwrite/fclose -- TODO.md section 3.1.
*
* Positioning, flushing, character and line I/O, stream state, formatted input,
* and the file-level operations that go with them.
*
* The recurring theme, and the reason most of these are worth wrapping at all,
* is that stdio reports failure through a return value that is easy to mistake
* for data. ftell(3) returns -1L, fgetc(3) returns EOF, fgets(3) returns NULL,
* and sscanf(3) returns a count that a caller has to compare against the number
* of conversions it wrote out by hand. Every one of those is a silent failure
* waiting for someone to forget the check once.
*
* Where a function has a genuine "nothing more to read" outcome -- fgets at the
* end of a file, getline at EOF -- that is AKERR_EOF rather than AKERR_IO, so a
* read loop can tell the end of its input from the failure of its input.
*/
#include <akstdlib.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include "aksl_internal.h"
/* ---------------------------------------------------------------------- */
/* Positioning */
/* ---------------------------------------------------------------------- */
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseek(FILE *stream, long offset, int whence)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, fseek(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO),
"fseek to offset %ld whence %d", offset, whence);
SUCCEED_RETURN(e);
}
/*
* ftell(3) reports failure as -1L, which is also a perfectly ordinary thing for
* an arithmetic type to hold. Out through *dest, with the failure as a status.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftell(FILE *stream, long *dest)
{
long pos = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = 0;
errno = 0;
pos = ftell(stream);
FAIL_NONZERO_RETURN(e, (pos == -1L), AKSL_ERRNO_OR(AKERR_IO), "ftell failed");
*dest = pos;
SUCCEED_RETURN(e);
}
/*
* rewind(3) is the one positioning call with no error return at all: it is
* fseek(stream, 0, SEEK_SET) with the result thrown away, and it clears the
* error indicator on the way past so even that evidence is gone. This is the
* fseek, so a rewind that cannot happen is reported rather than assumed.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_rewind(FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, fseek(stream, 0L, SEEK_SET), AKSL_ERRNO_OR(AKERR_IO),
"rewind failed");
clearerr(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fseeko(FILE *stream, off_t offset, int whence)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, fseeko(stream, offset, whence), AKSL_ERRNO_OR(AKERR_IO),
"fseeko to offset %lld whence %d", (long long)offset, whence);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_ftello(FILE *stream, off_t *dest)
{
off_t pos = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = 0;
errno = 0;
pos = ftello(stream);
FAIL_NONZERO_RETURN(e, (pos == (off_t)-1), AKSL_ERRNO_OR(AKERR_IO), "ftello failed");
*dest = pos;
SUCCEED_RETURN(e);
}
/*
* fgetpos/fsetpos carry an opaque fpos_t rather than a byte offset, which is
* what makes them the right pair for a stream in a multibyte locale where a
* byte offset is not enough to restore the conversion state.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetpos(FILE *stream, fpos_t *pos)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
errno = 0;
FAIL_NONZERO_RETURN(e, fgetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fgetpos failed");
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fsetpos(FILE *stream, const fpos_t *pos)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
FAIL_ZERO_RETURN(e, pos, AKERR_NULLPOINTER, "stream=%p, pos=%p", (void *)stream, (void *)pos);
errno = 0;
FAIL_NONZERO_RETURN(e, fsetpos(stream, pos), AKSL_ERRNO_OR(AKERR_IO), "fsetpos failed");
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Flushing and buffering */
/* ---------------------------------------------------------------------- */
/*
* NULL is legal here and means "every output stream", which is fflush(3)'s own
* documented behaviour and genuinely useful before a fork or an abort -- so
* unlike almost everywhere else in this library, a NULL argument is not an
* error. It is also the only way to find out that buffered data could not be
* written before the process goes away.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fflush(FILE *stream)
{
PREPARE_ERROR(e);
errno = 0;
FAIL_NONZERO_RETURN(e, fflush(stream), AKSL_ERRNO_OR(AKERR_IO),
"fflush failed and the buffered data is lost");
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_setvbuf(FILE *stream, char *buf, int mode, size_t size)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, setvbuf(stream, buf, mode, size), AKSL_ERRNO_OR(AKERR_VALUE),
"setvbuf mode %d size %zu", mode, size);
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Character and line I/O */
/* ---------------------------------------------------------------------- */
/*
* fgetc(3) folds three outcomes into one int: a byte, the end of the file, and
* a read error, the last two both spelled EOF. Split apart here -- *dest is the
* byte, AKERR_EOF is the end, and anything else is the error -- so a read loop
* can be written without the ferror/feof dance at the bottom of it.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgetc(FILE *stream, int *dest)
{
int c = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = 0;
errno = 0;
c = fgetc(stream);
if ( c == EOF ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgetc failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*dest = c;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputc(int c, FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, (fputc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO),
"fputc of 0x%02x failed", (unsigned)c & 0xffu);
SUCCEED_RETURN(e);
}
/* Pushes one byte back so the next read returns it. One byte is all that is
* guaranteed; a second ungetc without a read in between may well fail. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_ungetc(int c, FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, (ungetc(c, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO),
"ungetc of 0x%02x failed", (unsigned)c & 0xffu);
SUCCEED_RETURN(e);
}
/*
* fgets(3) into a bounded buffer, with the length read reported and the three
* outcomes separated. A line too long for the buffer is *not* an error -- it is
* a short read that leaves the rest of the line in the stream, which is how
* fgets works and how a caller reading fixed-size chunks wants it -- but
* *len_out lets the caller notice, because a full buffer with no trailing
* newline is exactly that case.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_fgets(char *s, size_t size, FILE *stream, size_t *len_out)
{
char *result = NULL;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
(void *)s, (void *)stream, (void *)len_out);
*len_out = 0;
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
(void *)s, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p, len_out=%p",
(void *)s, (void *)stream, (void *)len_out);
/* fgets takes an int, and a size that does not fit one is a caller error. */
FAIL_NONZERO_RETURN(e, (size == 0 || size > (size_t)INT_MAX), AKERR_VALUE,
"size %zu is outside the range fgets accepts", size);
s[0] = '\0';
errno = 0;
result = fgets(s, (int)size, stream);
if ( result == NULL ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fgets failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*len_out = strlen(s);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fputs(const char *s, FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "s=%p, stream=%p", (void *)s, (void *)stream);
errno = 0;
FAIL_NONZERO_RETURN(e, (fputs(s, stream) == EOF), AKSL_ERRNO_OR(AKERR_IO), "fputs failed");
SUCCEED_RETURN(e);
}
/*
* getline(3) grows *lineptr as needed, so the caller starts with
* `char *line = NULL; size_t cap = 0;` and releases the buffer with aksl_free
* once the loop is done -- not once per line. *len_out is the length read,
* which is what tells an embedded NUL from the end of the line; strlen cannot.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_getline(char **lineptr, size_t *n, FILE *stream, size_t *len_out)
{
ssize_t got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
*len_out = 0;
FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
errno = 0;
got = getline(lineptr, n, stream);
if ( got < 0 ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getline failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*len_out = (size_t)got;
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_getdelim(char **lineptr, size_t *n, int delim,
FILE *stream, size_t *len_out)
{
ssize_t got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, len_out, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
*len_out = 0;
FAIL_ZERO_RETURN(e, lineptr, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, n, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "lineptr=%p, n=%p, stream=%p, len_out=%p",
(void *)lineptr, (void *)n, (void *)stream, (void *)len_out);
errno = 0;
got = getdelim(lineptr, n, delim, stream);
if ( got < 0 ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "getdelim failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream");
}
*len_out = (size_t)got;
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Stream state */
/* ---------------------------------------------------------------------- */
/*
* feof and ferror answer through an out-param, exposed so that a caller never
* has to reach into a FILE * itself. They are thin, and that is the point: the
* NULL check is the whole value.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_feof(FILE *stream, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = feof(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_ferror(FILE *stream, int *dest)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = ferror(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_clearerr(FILE *stream)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p", (void *)stream);
clearerr(stream);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fileno(FILE *stream, int *dest)
{
int fd = -1;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stream=%p, dest=%p", (void *)stream, (void *)dest);
*dest = -1;
errno = 0;
fd = fileno(stream);
FAIL_NONZERO_RETURN(e, (fd < 0), AKSL_ERRNO_OR(EBADF), "stream has no descriptor");
*dest = fd;
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Opening by other means */
/* ---------------------------------------------------------------------- */
akerr_ErrorContext AKERR_NOIGNORE *aksl_freopen(const char *pathname, const char *mode,
FILE *stream, FILE **fp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
*fp = NULL;
FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "pathname=%p, mode=%p, stream=%p, fp=%p",
(void *)pathname, (void *)mode, (void *)stream, (void *)fp);
/*
* pathname NULL is legal and means "reopen the same file with a new mode",
* which is the one thing freopen can do that fopen cannot -- so it is not
* checked, unlike aksl_fopen's.
*/
errno = 0;
*fp = freopen(pathname, mode, stream);
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "%s", pathname != NULL ? pathname : "(same file)");
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fdopen(int fd, const char *mode, FILE **fp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp);
*fp = NULL;
FAIL_ZERO_RETURN(e, mode, AKERR_NULLPOINTER, "mode=%p, fp=%p", (void *)mode, (void *)fp);
FAIL_NONZERO_RETURN(e, (fd < 0), AKERR_VALUE, "fd=%d is not a descriptor", fd);
errno = 0;
*fp = fdopen(fd, mode);
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "fd=%d mode=%s", fd, mode);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_tmpfile(FILE **fp)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fp, AKERR_NULLPOINTER, "fp=%p", (void *)fp);
*fp = NULL;
errno = 0;
*fp = tmpfile();
FAIL_ZERO_RETURN(e, *fp, AKSL_ERRNO_OR(AKERR_IO), "could not create a temporary file");
SUCCEED_RETURN(e);
}
/* ---------------------------------------------------------------------- */
/* Formatted input */
/* ---------------------------------------------------------------------- */
/*
* The scanf family's return value is the number of conversions that succeeded,
* which the caller is expected to compare against the number it wrote in the
* format string -- by hand, from memory, at every call site. Getting that wrong
* leaves the unassigned arguments holding whatever they held before, which for
* the usual uninitialised local is anything at all.
*
* So `expected` is an argument: say how many conversions must succeed, and
* anything less is AKERR_VALUE with both counts in the message. Pass 0 to opt
* out and read the count from *assigned yourself.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_vsscanf(const char *str, const char *format,
int expected, int *assigned, va_list args)
{
int got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
(void *)str, (void *)format, (void *)assigned);
*assigned = 0;
FAIL_ZERO_RETURN(e, str, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
(void *)str, (void *)format, (void *)assigned);
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "str=%p, format=%p, assigned=%p",
(void *)str, (void *)format, (void *)assigned);
errno = 0;
got = vsscanf(str, format, args);
FAIL_NONZERO_RETURN(e, (got == EOF), AKSL_ERRNO_OR(AKERR_VALUE),
"no conversions were possible on \"%s\"", str);
*assigned = got;
FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE,
"%d of %d conversions succeeded on \"%s\"", got, expected, str);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_sscanf(const char *str, const char *format,
int expected, int *assigned, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, assigned);
raised = aksl_vsscanf(str, format, expected, assigned, args);
va_end(args);
return raised;
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_vfscanf(FILE *stream, const char *format,
int expected, int *assigned, va_list args)
{
int got = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, assigned, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
(void *)stream, (void *)format, (void *)assigned);
*assigned = 0;
FAIL_ZERO_RETURN(e, stream, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
(void *)stream, (void *)format, (void *)assigned);
FAIL_ZERO_RETURN(e, format, AKERR_NULLPOINTER, "stream=%p, format=%p, assigned=%p",
(void *)stream, (void *)format, (void *)assigned);
errno = 0;
got = vfscanf(stream, format, args);
if ( got == EOF ) {
FAIL_NONZERO_RETURN(e, ferror(stream), AKSL_ERRNO_OR(AKERR_IO), "fscanf failed");
FAIL_RETURN(e, AKERR_EOF, "end of stream before any conversion");
}
*assigned = got;
FAIL_NONZERO_RETURN(e, (expected > 0 && got < expected), AKERR_VALUE,
"%d of %d conversions succeeded", got, expected);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_fscanf(FILE *stream, const char *format,
int expected, int *assigned, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, assigned);
raised = aksl_vfscanf(stream, format, expected, assigned, args);
va_end(args);
return raised;
}
/* stdin, for symmetry with aksl_printf writing to stdout. */
akerr_ErrorContext AKERR_NOIGNORE *aksl_scanf(const char *format,
int expected, int *assigned, ...)
{
va_list args;
akerr_ErrorContext *raised = NULL;
va_start(args, assigned);
raised = aksl_vfscanf(stdin, format, expected, assigned, args);
va_end(args);
return raised;
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_vscanf(const char *format, int expected,
int *assigned, va_list args)
{
return aksl_vfscanf(stdin, format, expected, assigned, args);
}
/* ---------------------------------------------------------------------- */
/* Files */
/* ---------------------------------------------------------------------- */
akerr_ErrorContext AKERR_NOIGNORE *aksl_remove(const char *pathname)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "pathname=%p", (void *)pathname);
errno = 0;
FAIL_NONZERO_RETURN(e, remove(pathname), AKSL_ERRNO_OR(AKERR_IO), "%s", pathname);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_rename(const char *oldpath, const char *newpath)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, oldpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p",
(void *)oldpath, (void *)newpath);
FAIL_ZERO_RETURN(e, newpath, AKERR_NULLPOINTER, "oldpath=%p, newpath=%p",
(void *)oldpath, (void *)newpath);
errno = 0;
FAIL_NONZERO_RETURN(e, rename(oldpath, newpath), AKSL_ERRNO_OR(AKERR_IO),
"%s -> %s", oldpath, newpath);
SUCCEED_RETURN(e);
}
/*
* mkstemp and mkdtemp both rewrite the template in place, so the template must
* be a writable buffer ending in exactly six X characters -- a string literal
* is a segfault, and that is checked here rather than left to the kernel. The
* caller owns the resulting file or directory, including removing it.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkstemp(char *template_, int *fd)
{
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, fd, AKERR_NULLPOINTER, "template=%p, fd=%p", (void *)template_, (void *)fd);
*fd = -1;
FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p, fd=%p",
(void *)template_, (void *)fd);
len = strlen(template_);
FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0),
AKERR_VALUE,
"template \"%s\" must end in six literal X characters", template_);
errno = 0;
*fd = mkstemp(template_);
FAIL_NONZERO_RETURN(e, (*fd < 0), AKSL_ERRNO_OR(AKERR_IO), "template \"%s\"", template_);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_mkdtemp(char *template_)
{
size_t len = 0;
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, template_, AKERR_NULLPOINTER, "template=%p", (void *)template_);
len = strlen(template_);
FAIL_NONZERO_RETURN(e, (len < 6 || strcmp(template_ + len - 6, "XXXXXX") != 0),
AKERR_VALUE,
"template \"%s\" must end in six literal X characters", template_);
errno = 0;
FAIL_ZERO_RETURN(e, mkdtemp(template_), AKSL_ERRNO_OR(AKERR_IO),
"template \"%s\"", template_);
SUCCEED_RETURN(e);
}