/* * 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 #include #include #include #include #include #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 -- 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); }