Wrap the strings, streams and collections the wishlist asked for
TODO.md section 3.1's high-priority list and section 3.6's data
structures. This is the surface akbasic went without: it makes 10 calls
into this library and 116 to raw libc, and 69 of those 116 are strlen,
strcmp, strncpy and strstr.
Three new translation units, because src/stdlib.c covering four times
what it did would stop being readable:
src/string.c lengths, bounded copy and concatenation, duplication,
comparison including the case-insensitive forms,
searching, the reentrant tokenisers, and a status
message that knows this library's own statuses as
well as errno's.
src/stream.c positioning, flushing and buffering, character and
line I/O, stream state, freopen/fdopen/tmpfile,
formatted input, and the file operations.
src/collections.c the list functions that were missing, a head/tail
container so append is O(1), a binary search tree,
FNV-1a, a fixed-capacity hash map and a growable
string buffer.
Two conventions run through all of it. The copying functions take the
destination size even where the libc function they are named for does
not, because strcpy(3) cannot be called safely without it, and
truncation is an error that writes nothing rather than a plausible
prefix -- this is the idiom akbasic writes out by hand at ten sites.
The searching functions treat "not found" as a successful answer of
NULL, because absent is an answer and raising on it would make every
caller handle a non-error.
The hash map is akbasic's src/symtab.c generalised: open-addressed with
linear probing over a caller-supplied slot array, keys copied into fixed
slots so the map owns them, tombstones on delete so a removal cannot cut
a probe chain, and a refusal rather than a resize when full.
Tests: 16 binaries, green under the normal and sanitizer builds. The
scanf wrappers take the number of conversions the caller expects, since
comparing scanf(3)'s return against that by hand is the check everyone
eventually forgets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
25
src/aksl_internal.h
Normal file
25
src/aksl_internal.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#ifndef _AKSL_INTERNAL_H_
|
||||
#define _AKSL_INTERNAL_H_
|
||||
|
||||
/*
|
||||
* Shared internals. Not installed, not part of the public API -- anything here
|
||||
* is free to change without so much as a patch bump.
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
/*
|
||||
* errno as an akerror status, with a fallback for when it is not one.
|
||||
*
|
||||
* A libc call is allowed to fail without touching errno -- malloc(0) may return
|
||||
* NULL and leave it alone -- and errno may equally be a leftover from some
|
||||
* earlier, unrelated, *successful* call. Reporting it raw produces a FAIL whose
|
||||
* status is 0, which every downstream DETECT and CATCH reads as success while
|
||||
* the context still holds a pool slot: an error that is invisible and leaks at
|
||||
* the same time. Every errno-sourced status in this library goes through here,
|
||||
* and every wrapped call clears errno first so the value read back is its own.
|
||||
* TODO.md 2.2.1.
|
||||
*/
|
||||
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))
|
||||
|
||||
#endif // _AKSL_INTERNAL_H_
|
||||
1130
src/collections.c
Normal file
1130
src/collections.c
Normal file
File diff suppressed because it is too large
Load Diff
14
src/stdlib.c
14
src/stdlib.c
@@ -8,19 +8,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* errno as an akerror status, with a fallback for when it is not one.
|
||||
*
|
||||
* A libc call is allowed to fail without touching errno -- malloc(0) may return
|
||||
* NULL and leave it alone -- and errno may equally be a leftover from some
|
||||
* earlier, unrelated, *successful* call. Reporting it raw produced a FAIL whose
|
||||
* status was 0, which every downstream DETECT and CATCH reads as success while
|
||||
* the context still holds a pool slot: an error that is invisible and leaks at
|
||||
* the same time. Every errno-sourced status in this file goes through here, and
|
||||
* every wrapped call clears errno first so the value read back is its own.
|
||||
* TODO.md 2.2.1.
|
||||
*/
|
||||
#define AKSL_ERRNO_OR(__fallback) (errno != 0 ? errno : (__fallback))
|
||||
#include "aksl_internal.h"
|
||||
|
||||
/*
|
||||
* Version of the library itself. These read the AKSL_VERSION_* macros as they
|
||||
|
||||
551
src/stream.c
Normal file
551
src/stream.c
Normal file
@@ -0,0 +1,551 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* 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);
|
||||
}
|
||||
486
src/string.c
Normal file
486
src/string.c
Normal file
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
* string.h wrappers -- TODO.md section 3.1.
|
||||
*
|
||||
* This is the section akbasic needed most and could not have: across its source
|
||||
* it calls strlen 37 times, strcmp 16, strncpy 15 and strstr once, every one of
|
||||
* them raw because there was nothing here to call instead. Ten of those sites
|
||||
* are the same idiom written out by hand -- a length check, then strncpy, then
|
||||
* an explicit NUL -- which is exactly the "truncation reported as an error
|
||||
* rather than silently accepted" that TODO.md 3.1 asks for.
|
||||
*
|
||||
* Two conventions run through the whole file.
|
||||
*
|
||||
* The copying functions take the size of the destination, and the ones named
|
||||
* after libc functions that do not take one take it anyway. strcpy(3) and
|
||||
* strcat(3) cannot be called safely without knowing how much room the
|
||||
* destination has, and a wrapper that accepts the same arguments as strcpy
|
||||
* would be an error-handling wrapper around a buffer overflow. Truncation is
|
||||
* AKERR_OUTOFBOUNDS: it is the failure these functions exist to report, and
|
||||
* nothing is written to the destination when it happens, so a caller who
|
||||
* ignores the status does not get a half-copied string either. aksl_strncpy
|
||||
* also always terminates, which strncpy(3) famously does not.
|
||||
*
|
||||
* The searching functions answer through an out-param and treat "not found" as
|
||||
* a successful answer of NULL rather than as an error -- absent is an ordinary
|
||||
* answer to "where is this", and a library that raised on it would have every
|
||||
* caller handling a non-error as an error.
|
||||
*/
|
||||
|
||||
#include <akstdlib.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "aksl_internal.h"
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Length */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strlen(const char *s, size_t *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
*dest = strlen(s);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* strnlen(3) stops at maxlen whether or not it found a terminator, so a result
|
||||
* equal to maxlen means "at least this long" rather than "this long". That
|
||||
* ambiguity is the reason to use it -- it is how you measure a buffer that may
|
||||
* not be terminated -- so it is reported rather than hidden: *dest is the
|
||||
* length and the call succeeds either way.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strnlen(const char *s, size_t maxlen, size_t *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
*dest = strnlen(s, maxlen);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Copying and concatenation */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Bounded copy. dstsize is the whole destination buffer, terminator included,
|
||||
* so `char buf[64]` pairs with `sizeof(buf)`. A source that does not fit is
|
||||
* AKERR_OUTOFBOUNDS and the destination is left as an empty string rather than
|
||||
* as a truncated one -- a caller who ignores the error gets nothing, which is
|
||||
* far easier to notice than a plausible-looking prefix.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcpy(char *dst, size_t dstsize, const char *src)
|
||||
{
|
||||
size_t len = 0;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||
dst[0] = '\0';
|
||||
len = strlen(src);
|
||||
FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||
"source is %zu bytes and the destination holds %zu including the terminator",
|
||||
len, dstsize);
|
||||
memcpy(dst, src, len + 1);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* At most n bytes of src, and always terminated.
|
||||
*
|
||||
* strncpy(3) does two surprising things that this does not: it leaves the
|
||||
* destination unterminated when the source is at least n bytes long, and it
|
||||
* pads the remainder with NULs when the source is shorter, which turns a short
|
||||
* copy into a full-length write. Here n bounds how much of the source is
|
||||
* considered, dstsize bounds the write, and the result is always a C string.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncpy(char *dst, size_t dstsize, const char *src, size_t n)
|
||||
{
|
||||
size_t len = 0;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||
dst[0] = '\0';
|
||||
len = strnlen(src, n);
|
||||
FAIL_NONZERO_RETURN(e, (len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||
"%zu bytes to copy and the destination holds %zu including the terminator",
|
||||
len, dstsize);
|
||||
memcpy(dst, src, len);
|
||||
dst[len] = '\0';
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* Bounded append. dstsize is again the whole buffer, so the room actually
|
||||
* available is dstsize minus what is already in there. The destination must
|
||||
* already be a terminated string within dstsize; if it is not, that is
|
||||
* AKERR_VALUE rather than a walk off the end looking for a NUL that is not
|
||||
* there.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcat(char *dst, size_t dstsize, const char *src)
|
||||
{
|
||||
size_t used = 0;
|
||||
size_t len = 0;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||
used = strnlen(dst, dstsize);
|
||||
FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE,
|
||||
"destination is not terminated within its %zu bytes", dstsize);
|
||||
len = strlen(src);
|
||||
FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||
"%zu bytes in use plus %zu to append exceeds the %zu-byte destination",
|
||||
used, len, dstsize);
|
||||
memcpy(dst + used, src, len + 1);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncat(char *dst, size_t dstsize, const char *src, size_t n)
|
||||
{
|
||||
size_t used = 0;
|
||||
size_t len = 0;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, dst, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, src, AKERR_NULLPOINTER, "dst=%p, src=%p", (void *)dst, (void *)src);
|
||||
FAIL_ZERO_RETURN(e, dstsize, AKERR_VALUE, "dstsize=0 leaves no room for a terminator");
|
||||
used = strnlen(dst, dstsize);
|
||||
FAIL_NONZERO_RETURN(e, (used == dstsize), AKERR_VALUE,
|
||||
"destination is not terminated within its %zu bytes", dstsize);
|
||||
len = strnlen(src, n);
|
||||
FAIL_NONZERO_RETURN(e, (used + len >= dstsize), AKERR_OUTOFBOUNDS,
|
||||
"%zu bytes in use plus %zu to append exceeds the %zu-byte destination",
|
||||
used, len, dstsize);
|
||||
memcpy(dst + used, src, len);
|
||||
dst[used + len] = '\0';
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Duplication */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/* *dest is the caller's, to release with aksl_free or aksl_freep. */
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strdup(const char *s, char **dest)
|
||||
{
|
||||
char *copy = NULL;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
*dest = NULL;
|
||||
errno = 0;
|
||||
copy = strdup(s);
|
||||
FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", strlen(s) + 1);
|
||||
*dest = copy;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strndup(const char *s, size_t n, char **dest)
|
||||
{
|
||||
char *copy = NULL;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
*dest = NULL;
|
||||
errno = 0;
|
||||
copy = strndup(s, n);
|
||||
FAIL_ZERO_RETURN(e, copy, AKSL_ERRNO_OR(ENOMEM), "%zu bytes", n + 1);
|
||||
*dest = copy;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Comparison */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* The comparison result goes through *dest because the return value is spoken
|
||||
* for by the error context. The sign is the usual one: negative, zero or
|
||||
* positive as a sorts before, equal to, or after b.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcmp(const char *a, const char *b, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
*dest = strcmp(a, b);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncmp(const char *a, const char *b, size_t n, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
*dest = strncmp(a, b, n);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* Case-insensitive comparison. akbasic folds case by hand at three sites --
|
||||
* twice with an open-coded loop and once through <ctype.h> -- because BASIC
|
||||
* verb and function names are case-insensitive while variable names are not.
|
||||
* All three of those are this call.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasecmp(const char *a, const char *b, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
*dest = strcasecmp(a, b);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strncasecmp(const char *a, const char *b, size_t n, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
*dest = strncasecmp(a, b, n);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* Locale-aware collation. strcoll(3) has no error return of its own, so a
|
||||
* malformed multibyte sequence in the current locale shows up only as errno
|
||||
* being set -- which is why errno is cleared first and consulted after.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcoll(const char *a, const char *b, int *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, a, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, b, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "a=%p, b=%p, dest=%p", (void *)a, (void *)b, (void *)dest);
|
||||
errno = 0;
|
||||
*dest = strcoll(a, b);
|
||||
FAIL_NONZERO_RETURN(e, errno, errno, "strcoll failed in the current locale");
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Searching */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Every function in this section writes NULL to *dest and succeeds when there
|
||||
* is nothing to find. "Absent" is an answer, not a failure; raising on it would
|
||||
* make every caller handle a non-error as an error, and the pool slot that
|
||||
* error consumed would be pure waste.
|
||||
*
|
||||
* *dest points into the caller's own string, so it lives exactly as long as the
|
||||
* string does and must not be freed.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strchr(const char *s, int c, char **dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
*dest = strchr(s, c);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strrchr(const char *s, int c, char **dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, dest=%p", (void *)s, (void *)dest);
|
||||
*dest = strrchr(s, c);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strstr(const char *haystack, const char *needle, char **dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||
(void *)haystack, (void *)needle, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||
(void *)haystack, (void *)needle, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||
(void *)haystack, (void *)needle, (void *)dest);
|
||||
*dest = strstr(haystack, needle);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* Case-insensitive search. strcasestr(3) is a GNU extension that also exists on
|
||||
* the BSDs and musl, but it is not in any standard, so it is open-coded here
|
||||
* rather than depending on _GNU_SOURCE reaching every consumer's build. The
|
||||
* naive scan is the same complexity as glibc's fallback and this is not the hot
|
||||
* path in anything.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcasestr(const char *haystack, const char *needle, char **dest)
|
||||
{
|
||||
size_t hlen = 0;
|
||||
size_t nlen = 0;
|
||||
size_t i = 0;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, haystack, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||
(void *)haystack, (void *)needle, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, needle, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||
(void *)haystack, (void *)needle, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "haystack=%p, needle=%p, dest=%p",
|
||||
(void *)haystack, (void *)needle, (void *)dest);
|
||||
*dest = NULL;
|
||||
nlen = strlen(needle);
|
||||
/* An empty needle matches at the start, as strstr(3) has it. */
|
||||
if ( nlen == 0 ) {
|
||||
*dest = (char *)haystack;
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
hlen = strlen(haystack);
|
||||
if ( nlen <= hlen ) {
|
||||
for ( i = 0; i <= hlen - nlen; i++ ) {
|
||||
if ( strncasecmp(haystack + i, needle, nlen) == 0 ) {
|
||||
*dest = (char *)(haystack + i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strpbrk(const char *s, const char *accept, char **dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||
(void *)s, (void *)accept, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||
(void *)s, (void *)accept, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||
(void *)s, (void *)accept, (void *)dest);
|
||||
*dest = strpbrk(s, accept);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strspn(const char *s, const char *accept, size_t *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||
(void *)s, (void *)accept, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, accept, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||
(void *)s, (void *)accept, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, accept=%p, dest=%p",
|
||||
(void *)s, (void *)accept, (void *)dest);
|
||||
*dest = strspn(s, accept);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strcspn(const char *s, const char *reject, size_t *dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, s, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
|
||||
(void *)s, (void *)reject, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, reject, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
|
||||
(void *)s, (void *)reject, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "s=%p, reject=%p, dest=%p",
|
||||
(void *)s, (void *)reject, (void *)dest);
|
||||
*dest = strcspn(s, reject);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Tokenising */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* The reentrant tokeniser only. strtok(3) keeps its state in a hidden static,
|
||||
* which makes two interleaved tokenisations silently corrupt each other and
|
||||
* makes any use from a thread a bug, so it is not wrapped here at all -- see
|
||||
* the note on threads in README.md.
|
||||
*
|
||||
* Running out of tokens is success with *dest NULL, for the same reason a failed
|
||||
* search is: it is how the loop ends, not a fault.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strtok_r(char *str, const char *delim, char **saveptr, char **dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
|
||||
(void *)delim, (void *)saveptr, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, saveptr, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
|
||||
(void *)delim, (void *)saveptr, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "delim=%p, saveptr=%p, dest=%p",
|
||||
(void *)delim, (void *)saveptr, (void *)dest);
|
||||
*dest = strtok_r(str, delim, saveptr);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/*
|
||||
* strsep(3) differs from strtok_r in returning empty tokens between adjacent
|
||||
* delimiters, which is what you want for parsing "a::b" as three fields rather
|
||||
* than two. It advances *stringp itself and sets it to NULL when done.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strsep(char **stringp, const char *delim, char **dest)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, stringp, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
|
||||
(void *)stringp, (void *)delim, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, delim, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
|
||||
(void *)stringp, (void *)delim, (void *)dest);
|
||||
FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "stringp=%p, delim=%p, dest=%p",
|
||||
(void *)stringp, (void *)delim, (void *)dest);
|
||||
*dest = strsep(stringp, delim);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* errno messages */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* The message for a status, into the caller's own buffer.
|
||||
*
|
||||
* Deliberately not built on strerror_r(3). There are two incompatible functions
|
||||
* by that name -- the XSI one returns int, the GNU one returns char * and may
|
||||
* not touch the buffer at all -- and which one a translation unit gets depends
|
||||
* on feature-test macros that a consumer of this library cannot influence from
|
||||
* in here. strerror(3) itself is not thread-safe, so that is no way out either.
|
||||
*
|
||||
* libakerror already carries the full errno name table (its generrno.sh builds
|
||||
* one at configure time), and its registry also knows the names of this
|
||||
* library's own non-errno statuses -- AKERR_NULLPOINTER, AKERR_ITERATOR_BREAK
|
||||
* and the rest -- which strerror_r could never name. So the lookup goes there,
|
||||
* and a status neither table recognises comes back as its own number rather
|
||||
* than as the bare string "Unknown Error", which tells the reader nothing.
|
||||
*
|
||||
* The message is truncated to nothing rather than to a prefix if it does not
|
||||
* fit, on the same principle as aksl_strcpy above.
|
||||
*/
|
||||
akerr_ErrorContext AKERR_NOIGNORE *aksl_strerror(int status, char *buf, size_t buflen)
|
||||
{
|
||||
const char *name = NULL;
|
||||
int needed = 0;
|
||||
PREPARE_ERROR(e);
|
||||
FAIL_ZERO_RETURN(e, buf, AKERR_NULLPOINTER, "buf=%p", (void *)buf);
|
||||
FAIL_ZERO_RETURN(e, buflen, AKERR_VALUE, "buflen=0 leaves no room for a terminator");
|
||||
buf[0] = '\0';
|
||||
name = akerr_name_for_status(status, NULL);
|
||||
/*
|
||||
* akerr_name_for_status never returns NULL; it returns this exact string
|
||||
* for anything it does not have a name for.
|
||||
*/
|
||||
if ( name == NULL || strcmp(name, "Unknown Error") == 0 ) {
|
||||
needed = snprintf(buf, buflen, "Unknown status %d", status);
|
||||
} else {
|
||||
needed = snprintf(buf, buflen, "%s", name);
|
||||
}
|
||||
FAIL_NONZERO_RETURN(e, (needed < 0), AKERR_IO, "could not format status %d", status);
|
||||
if ( (size_t)needed >= buflen ) {
|
||||
buf[0] = '\0';
|
||||
FAIL_RETURN(e, AKERR_OUTOFBOUNDS,
|
||||
"the message for status %d needs %d bytes and the buffer holds %zu",
|
||||
status, needed + 1, buflen);
|
||||
}
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
Reference in New Issue
Block a user