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>
571 lines
23 KiB
C
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);
|
|
}
|