diff --git a/include/string.h b/include/string.h index 2ac6874..88778b0 100644 --- a/include/string.h +++ b/include/string.h @@ -7,8 +7,14 @@ * String and memory functions, gated by the active compatibility profile * (see include/vlibc/features.h). Levels are cumulative: * - * Level 1 (onlyposix): ISO C core — memcpy, memmove, memset, strlen, strcmp. - * Level 2 (muslmimic): BSD extensions — strlcpy, strlcat. + * Level 1 (onlyposix): ISO C core + POSIX.1-2008 base — memcpy, memmove, + * memset, memchr, memcmp, strlen, strnlen, strcmp, + * strncmp, strcpy, strncpy, strcat, strncat, strchr, + * strrchr, strspn, strcspn, strpbrk, strstr, strtok, + * strtok_r, strcoll, strxfrm, strdup, strndup, + * strsignal. + * Level 2 (muslmimic): BSD + XSI extensions — strlcpy, strlcat, stpcpy, + * stpncpy, memccpy, strcoll_l, strxfrm_l. * Level 3 (muslext): GNU extensions — strcasestr. * * This header includes itself, so the gates below always @@ -33,6 +39,15 @@ extern "C" { __attribute__((pure)) size_t strlen(const char *s); +/* + * Return the length of s, excluding the terminating NUL, examining at most + * maxlen bytes; the result is at most maxlen even when s is not a proper + * NUL-terminated string. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) size_t +strnlen(const char *s, size_t maxlen); + /* * Compare the strings lhs and rhs; return negative, zero, or positive when * lhs is less than, equal to, or greater than rhs. @@ -41,6 +56,24 @@ strlen(const char *s); __attribute__((pure)) int strcmp(const char *lhs, const char *rhs); +/* + * Compare at most n bytes of lhs and rhs, stopping early at the first + * difference or the first NUL; return negative, zero, or positive when lhs + * is less than, equal to, or greater than rhs. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) int +strncmp(const char *lhs, const char *rhs, size_t n); + +/* + * Compare s1 and s2 under the active locale's collating sequence; return + * negative, zero, or positive. Only the "C" locale exists so far (locales + * are owned by a later todo), where collation is identical to strcmp. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) int +strcoll(const char *s1, const char *s2); + /* * Copy n bytes from src to dst. The regions must not overlap (restrict). * No intent attribute: it writes memory. @@ -63,6 +96,157 @@ memmove(void *dst, const void *src, size_t n); void * memset(void *dst, int c, size_t n); +/* + * Return a pointer to the first occurrence of c (converted to unsigned + * char) among the first n bytes of s, or NULL when absent. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) void * +memchr(const void *s, int c, size_t n); + +/* + * Compare the first n bytes of lhs and rhs as unsigned char; return + * negative, zero, or positive when lhs is less than, equal to, or greater + * than rhs. Unlike strcmp, a NUL byte does not end the comparison. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) int +memcmp(const void *lhs, const void *rhs, size_t n); + +/* + * Copy src to dst, including the terminating NUL; return dst. The strings + * must not overlap (restrict). + * No intent attribute: it writes memory. + */ +char * +strcpy(char *restrict dst, const char *restrict src); + +/* + * Copy at most n bytes from src to dst. When src is shorter than n the + * remainder of dst is NUL-padded; when src is n bytes or longer the result + * is not NUL-terminated. Return dst. + * No intent attribute: it writes memory. + */ +char * +strncpy(char *restrict dst, const char *restrict src, size_t n); + +/* + * Append src (including its NUL) to the end of dst; return dst. The strings + * must not overlap (restrict). + * No intent attribute: it writes memory. + */ +char * +strcat(char *restrict dst, const char *restrict src); + +/* + * Append at most n bytes of src to dst and always NUL-terminate; return + * dst. + * No intent attribute: it writes memory. + */ +char * +strncat(char *restrict dst, const char *restrict src, size_t n); + +/* + * Return a pointer to the first occurrence of c (converted to char) in s, + * or NULL when absent. The terminating NUL is part of the string, so + * strchr(s, '\0') returns a pointer to it. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) char * +strchr(const char *s, int c); + +/* + * Return a pointer to the last occurrence of c (converted to char) in s, or + * NULL when absent. The terminating NUL is part of the string, so + * strrchr(s, '\0') returns a pointer to it. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) char * +strrchr(const char *s, int c); + +/* + * Return the length of the initial span of s consisting entirely of bytes + * that occur in accept. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) size_t +strspn(const char *s, const char *accept); + +/* + * Return the length of the initial span of s consisting entirely of bytes + * that do NOT occur in reject. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) size_t +strcspn(const char *s, const char *reject); + +/* + * Return a pointer to the first byte in s that also occurs in accept, or + * NULL when none occurs. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) char * +strpbrk(const char *s, const char *accept); + +/* + * Return a pointer to the first occurrence of needle in haystack, or NULL + * when absent. An empty needle matches haystack itself. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) char * +strstr(const char *haystack, const char *needle); + +/* + * Split s into tokens delimited by any byte from sep. On the first call s + * names the string; subsequent calls with NULL continue the same string. + * Leading and consecutive delimiters produce no empty tokens, and the + * delimiter bytes in s are overwritten with NUL. Returns NULL when no token + * remains. strtok keeps its state in private static storage and is not + * thread-safe; strtok_r keeps it in *state and is. + * No intent attribute: strtok mutates private state, strtok_r writes + * through its parameters. + */ +char * +strtok(char *restrict s, const char *restrict sep); + +char * +strtok_r(char *restrict s, const char *restrict sep, char **restrict state); + +/* + * Transform src under the active locale's collating sequence so that strcmp + * on transformed strings orders them as strcoll would, storing at most n + * bytes of the result in dst (always NUL-terminated when n > 0; nothing is + * written when n == 0). Return the length of the full transformed string, + * excluding the NUL. In the "C" locale the transformation is the identity + * and the return is strlen(src). + * No intent attribute: it writes memory. + */ +size_t +strxfrm(char *restrict dst, const char *restrict src, size_t n); + +/* + * Return a heap copy of s (malloc-allocated; release with free), or NULL + * with errno ENOMEM on allocation failure. strndup copies at most n bytes + * and NUL-terminates, so strndup(s, 0) returns the empty string. + * malloc: returns fresh unaliased storage the caller owns. + */ +__attribute__((malloc)) char * +strdup(const char *s); + +__attribute__((malloc)) char * +strndup(const char *s, size_t n); + +/* + * Return a pointer to a static string describing the signal sig, or a + * formatted "Unknown signal " string for unrecognized numbers. Never + * returns NULL; distinct known signals yield distinct strings. The text for + * signal 0 is unspecified by POSIX and deliberately not pinned. + * + * No intent attribute: the unknown-signal path writes shared storage. + */ +char * +strsignal(int sig); + #if VLIBC_LEVEL >= 2 /* Level 2 (muslmimic): BSD extensions. */ @@ -83,6 +267,46 @@ size_t strlcat(char *dst, const char *src, size_t size); #endif /* VLIBC_LEVEL >= 2 */ +#if VLIBC_LEVEL_GE(2) +/* Level 2 (muslmimic): XSI extensions. */ + +/* + * strcpy/strncpy variants returning a pointer to the terminating NUL of + * dst: stpcpy to the NUL of the copied string, stpncpy to the first NUL + * written (or to dst + n when no NUL was written). + * No intent attribute: they write memory. + */ +char * +stpcpy(char *restrict dst, const char *restrict src); + +char * +stpncpy(char *restrict dst, const char *restrict src, size_t n); + +/* + * Copy at most n bytes from src to dst, stopping after the first byte equal + * to c (converted to unsigned char); return a pointer to the byte after c + * in dst when found, NULL otherwise. The regions must not overlap + * (restrict). + * No intent attribute: it writes memory. + */ +void * +memccpy(void *restrict dst, const void *restrict src, int c, size_t n); + +/* + * Locale-parameterized XSI variants of strcoll/strxfrm. No locale machinery + * exists yet, so the locale argument is accepted and ignored and behavior + * is the "C" locale behavior of the base functions. The locale parameter is + * typed void * for now: locale_t will be an ABI-identical pointer typedef + * defined by , whose todo updates these signatures to the real + * type. + */ +__attribute__((pure)) int +strcoll_l(const char *s1, const char *s2, void *locale); + +size_t +strxfrm_l(char *restrict dst, const char *restrict src, size_t n, void *locale); +#endif /* VLIBC_LEVEL_GE(2) */ + #if VLIBC_LEVEL >= 3 /* Level 3 (muslext): GNU extensions. */ diff --git a/src/internal/malloc.h b/src/internal/malloc.h new file mode 100644 index 0000000..e070645 --- /dev/null +++ b/src/internal/malloc.h @@ -0,0 +1,30 @@ +#ifndef VLIBC_INTERNAL_MALLOC_H +#define VLIBC_INTERNAL_MALLOC_H + +/* + * vlibc — internal allocator seam (todo 8). + * + * The allocator todo (#7) owns the real implementation and provides these + * entry points; library code that must allocate (strdup, strndup, and later + * stdio/regex/...) calls them instead of the public malloc/free so that the + * allocator remains the single allocation implementation. The public + * malloc/free/calloc/realloc of todo 7 must be backed by the same allocator, + * so a block returned by __libc_malloc can be released with public free. + * + * - __libc_malloc(n): allocate n bytes, 16-byte aligned; NULL + errno ENOMEM + * on failure. May return a unique pointer even when n == 0. + * - __libc_free(p): release a block returned by __libc_malloc; NULL is a + * no-op. + */ + +#include + +#include "libc.h" + +hidden void * +__libc_malloc(size_t n); // NOLINT(bugprone-reserved-identifier) + +hidden void +__libc_free(void *p); // NOLINT(bugprone-reserved-identifier) + +#endif /* VLIBC_INTERNAL_MALLOC_H */ diff --git a/src/string/memccpy.c b/src/string/memccpy.c new file mode 100644 index 0000000..fdb2bf7 --- /dev/null +++ b/src/string/memccpy.c @@ -0,0 +1,40 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#if VLIBC_LEVEL_GE(2) + +/* + * XSI memccpy (todo 8): copy at most n bytes from src to dst, stopping + * after the first byte equal to c (converted to unsigned char). Return a + * pointer to the byte after c in dst when found, NULL otherwise. The + * regions must not overlap (restrict). + * + * See strcpy.c: the copy loop is a recognizable pattern, so disable + * -ftree-loop-distribute-patterns here too. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) void * +memccpy(void *restrict dst, const void *restrict src, int c, size_t n) // NOLINT(bugprone-*) +{ + unsigned char *d = dst; + const unsigned char *s = src; + const unsigned char uc = (unsigned char)c; + + while (n != 0) + { + *d = *s; + if (*d == uc) + { + return d + 1; + } + d++; + s++; + n--; + } + + return NULL; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/string/memchr.c b/src/string/memchr.c new file mode 100644 index 0000000..3b6defe --- /dev/null +++ b/src/string/memchr.c @@ -0,0 +1,34 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Return a pointer to the first occurrence of c (converted to unsigned + * char) among the first n bytes of s, or NULL when absent. Bytes are + * examined as unsigned char so negative c values search for the matching + * 0x80..0xff byte. + * + * See strcpy.c: the scan loop is a pattern GCC's + * -ftree-loop-distribute-patterns can recognize (memchr), so disable that + * transformation to avoid self-recursion. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) void * +memchr(const void *s, int c, size_t n) // NOLINT(bugprone-easily-swappable-parameters) +{ + const unsigned char *p = s; + const unsigned char uc = (unsigned char)c; + + while (n != 0) + { + if (*p == uc) + { + return (void *)p; + } + p++; + n--; + } + + return NULL; +} diff --git a/src/string/memcmp.c b/src/string/memcmp.c new file mode 100644 index 0000000..68b7163 --- /dev/null +++ b/src/string/memcmp.c @@ -0,0 +1,34 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Compare the first n bytes of lhs and rhs as unsigned char; return + * negative, zero, or positive when lhs is less than, equal to, or greater + * than rhs. Unlike strcmp the comparison never stops at a NUL byte. + * + * See strcpy.c: the compare loop is a pattern GCC's + * -ftree-loop-distribute-patterns can recognize (memcmp), so disable that + * transformation to avoid self-recursion. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) int +memcmp(const void *lhs, const void *rhs, size_t n) // NOLINT(bugprone-easily-swappable-parameters) +{ + const unsigned char *l = lhs; + const unsigned char *r = rhs; + + while (n != 0) + { + if (*l != *r) + { + return *l < *r ? -1 : 1; + } + l++; + r++; + n--; + } + + return 0; +} diff --git a/src/string/stpcpy.c b/src/string/stpcpy.c new file mode 100644 index 0000000..12a7a45 --- /dev/null +++ b/src/string/stpcpy.c @@ -0,0 +1,69 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#if VLIBC_LEVEL_GE(2) + +/* + * XSI stpcpy / stpncpy (todo 8): strcpy / strncpy variants that return a + * pointer to the terminating NUL of dst instead of dst itself. + * + * See strcpy.c: the copy loops are recognizable patterns, so disable + * -ftree-loop-distribute-patterns here too. + */ + +/* + * Copy src to dst including the terminating NUL; return a pointer to the + * NUL written into dst. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) char * +stpcpy(char *restrict dst, const char *restrict src) // NOLINT(bugprone-*) +{ + char *d = dst; + + for (;;) + { + *d = *src; + if (*d == '\0') + { + return d; + } + d++; + src++; + } +} + +/* + * Copy at most n bytes from src to dst, NUL-padding when src is shorter; + * return a pointer to the first NUL written into dst, or to dst + n when no + * NUL was written. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) char * +stpncpy(char *restrict dst, const char *restrict src, size_t n) // NOLINT(bugprone-*) +{ + char *d = dst; + char *end; + + /* Copy until src ends or n bytes have been copied. */ + while (n != 0 && *src != '\0') + { + *d++ = *src++; + n--; + } + + /* The first NUL (or one past the last byte) sits right here. */ + end = d; + + /* NUL-pad the remainder. */ + while (n != 0) + { + *d++ = '\0'; + n--; + } + + return end; +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/string/strcat.c b/src/string/strcat.c new file mode 100644 index 0000000..dd68cda --- /dev/null +++ b/src/string/strcat.c @@ -0,0 +1,65 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Append src (including its terminating NUL) to the end of dst; return dst. + * The strings must not overlap (restrict). + * + * See strcpy.c: the copy loop is a recognizable pattern, so disable + * -ftree-loop-distribute-patterns here too. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) char * +strcat(char *restrict dst, const char *restrict src) // NOLINT(bugprone-*) +{ + char *d = dst; + + /* Find the terminating NUL of dst. */ + while (*d != '\0') + { + d++; + } + + /* Append src including its NUL. */ + for (;;) + { + *d++ = *src; + if (*src == '\0') + { + return dst; + } + src++; + } +} + +/* + * Append at most n bytes of src to dst and always NUL-terminate; return dst. + * + * See strcpy.c: the copy loop is a recognizable pattern, so disable + * -ftree-loop-distribute-patterns here too. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) char * +strncat(char *restrict dst, const char *restrict src, size_t n) // NOLINT(bugprone-*) +{ + char *d = dst; + + /* Find the terminating NUL of dst. */ + while (*d != '\0') + { + d++; + } + + /* Append at most n bytes of src, stopping early at its NUL. */ + while (n != 0 && *src != '\0') + { + *d++ = *src++; + n--; + } + + /* strncat always NUL-terminates. */ + *d = '\0'; + + return dst; +} diff --git a/src/string/strchr.c b/src/string/strchr.c new file mode 100644 index 0000000..0fd9bdc --- /dev/null +++ b/src/string/strchr.c @@ -0,0 +1,52 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Return a pointer to the first occurrence of c (converted to char) in s, + * or NULL when absent. The terminating NUL is part of the string, so + * strchr(s, '\0') returns a pointer to it. + */ +char * +strchr(const char *s, int c) // NOLINT(bugprone-easily-swappable-parameters) +{ + const char cc = (char)c; + + for (;; s++) + { + if (*s == cc) + { + return (char *)s; + } + if (*s == '\0') + { + return NULL; + } + } +} + +/* + * Return a pointer to the last occurrence of c (converted to char) in s, or + * NULL when absent. The terminating NUL is part of the string, so + * strrchr(s, '\0') returns a pointer to it. + */ +char * +strrchr(const char *s, int c) // NOLINT(bugprone-easily-swappable-parameters) +{ + const char cc = (char)c; + const char *last = NULL; + + for (;; s++) + { + if (*s == cc) + { + last = s; + } + if (*s == '\0') + { + return (char *)last; + } + } +} diff --git a/src/string/strcoll.c b/src/string/strcoll.c new file mode 100644 index 0000000..86dcbf1 --- /dev/null +++ b/src/string/strcoll.c @@ -0,0 +1,79 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * C-locale collation (todo 49 owns real locales). In the "C" locale the + * collating sequence is the machine collating sequence — byte order — so + * strcoll degenerates to strcmp, and the strxfrm transformation is the + * identity: the transformed form of a string is the string itself. + */ + +/* + * Compare s1 and s2 under the active locale's collating sequence and return + * negative, zero, or positive. "C" locale: identical to strcmp. + */ +int +strcoll(const char *s1, const char *s2) // NOLINT(bugprone-easily-swappable-parameters) +{ + return strcmp(s1, s2); +} + +/* + * Transform src so that strcmp on transformed strings orders them as + * strcoll would, storing at most n bytes of the result in dst (always + * NUL-terminated when n > 0; nothing is written when n == 0). Return the + * length of the full transformed string excluding the NUL — in the "C" + * locale, strlen(src). + */ +size_t +strxfrm(char *restrict dst, const char *restrict src, + size_t n) // NOLINT(bugprone-easily-swappable-parameters) +{ + size_t len = strlen(src); + size_t copy; + size_t i; + + if (n == 0) + { + return len; + } + + copy = len < n ? len : n - 1; + for (i = 0; i < copy; i++) + { + dst[i] = src[i]; + } + dst[copy] = '\0'; + + return len; +} + +#if VLIBC_LEVEL_GE(2) +/* + * XSI locale-parameterized variants. No locale machinery exists yet (todo + * 49), so the locale argument is accepted and ignored and the behavior is + * the "C" locale behavior of the base functions. The locale parameter is + * typed void * for now: locale_t will be an ABI-identical pointer typedef + * defined by , and todo 49 updates these signatures to the real + * type. + */ + +int +strcoll_l(const char *s1, const char *s2, + void *locale) // NOLINT(bugprone-easily-swappable-parameters) +{ + (void)locale; + return strcoll(s1, s2); +} + +size_t +strxfrm_l(char *restrict dst, const char *restrict src, size_t n, + void *locale) // NOLINT(bugprone-easily-swappable-parameters) +{ + (void)locale; + return strxfrm(dst, src, n); +} +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/string/strcpy.c b/src/string/strcpy.c new file mode 100644 index 0000000..23d5ada --- /dev/null +++ b/src/string/strcpy.c @@ -0,0 +1,31 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Copy src to dst including the terminating NUL; return dst. The strings + * must not overlap (restrict). + * + * The copy loop below is the canonical strcpy idiom; GCC's + * -ftree-loop-distribute-patterns (on by default at -O2 and -O3) can rewrite + * such loops into calls to the recognized string builtins — potentially this + * very function — causing infinite self-recursion. Disable that one + * transformation for this function only. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) char * +strcpy(char *restrict dst, const char *restrict src) // NOLINT(bugprone-*) +{ + char *d = dst; + + for (;;) + { + *d++ = *src; + if (*src == '\0') + { + return dst; + } + src++; + } +} diff --git a/src/string/strdup.c b/src/string/strdup.c new file mode 100644 index 0000000..e50368d --- /dev/null +++ b/src/string/strdup.c @@ -0,0 +1,69 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "../internal/malloc.h" + +/* + * strdup / strndup (todo 8). + * + * Both allocate through the internal allocator seam __libc_malloc (see + * src/internal/malloc.h; todo 7 provides the real allocator), so the copies + * are released with the public free. On allocation failure errno is set to + * ENOMEM and NULL is returned. + */ + +/* + * Return a heap copy of s, or NULL with errno ENOMEM on allocation failure. + */ +char * +strdup(const char *s) +{ + size_t len = strlen(s) + 1; + char *p = __libc_malloc(len); + size_t i; + + if (p == NULL) + { + errno = ENOMEM; + return NULL; + } + + for (i = 0; i < len; i++) + { + p[i] = s[i]; + } + + return p; +} + +/* + * Return a heap copy of the first n bytes of s, NUL-terminated. When s is + * shorter than n the copy is the whole string; strndup(s, 0) returns the + * empty string. NULL with errno ENOMEM on allocation failure. + */ +char * +strndup(const char *s, size_t n) +{ + size_t len = strnlen(s, n); + char *p = __libc_malloc(len + 1); + size_t i; + + if (p == NULL) + { + errno = ENOMEM; + return NULL; + } + + for (i = 0; i < len; i++) + { + p[i] = s[i]; + } + p[len] = '\0'; + + return p; +} diff --git a/src/string/strncmp.c b/src/string/strncmp.c new file mode 100644 index 0000000..bf25218 --- /dev/null +++ b/src/string/strncmp.c @@ -0,0 +1,36 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Compare at most n bytes of lhs and rhs as unsigned char, stopping early + * at the first difference or the first NUL of either string. Return + * negative, zero, or positive when lhs is less than, equal to, or greater + * than rhs. + * + * See strcpy.c: the scan/compare loop is a pattern GCC's + * -ftree-loop-distribute-patterns can recognize (strcmp/strncmp), so disable + * that transformation to avoid self-recursion. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) int +strncmp(const char *lhs, const char *rhs, size_t n) // NOLINT(bugprone-easily-swappable-parameters) +{ + const unsigned char *l = (const unsigned char *)lhs; + const unsigned char *r = (const unsigned char *)rhs; + + while (n != 0 && *l != '\0' && *l == *r) + { + l++; + r++; + n--; + } + + if (n == 0 || *l == *r) + { + return 0; + } + + return *l < *r ? -1 : 1; +} diff --git a/src/string/strncpy.c b/src/string/strncpy.c new file mode 100644 index 0000000..9816d2a --- /dev/null +++ b/src/string/strncpy.c @@ -0,0 +1,35 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Copy at most n bytes from src to dst. When src is shorter than n, the + * remainder of dst is filled with NULs; when src is n bytes or longer, no + * NUL is written at all. Return dst. + * + * See strcpy.c: the copy loop is a recognizable pattern, so disable + * -ftree-loop-distribute-patterns here too. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) char * +strncpy(char *restrict dst, const char *restrict src, size_t n) // NOLINT(bugprone-*) +{ + char *d = dst; + + /* Copy until src ends or n bytes have been copied. */ + while (n != 0 && *src != '\0') + { + *d++ = *src++; + n--; + } + + /* NUL-pad the remainder (this also terminates a short src). */ + while (n != 0) + { + *d++ = '\0'; + n--; + } + + return dst; +} diff --git a/src/string/strnlen.c b/src/string/strnlen.c new file mode 100644 index 0000000..22e49b4 --- /dev/null +++ b/src/string/strnlen.c @@ -0,0 +1,28 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Return the length of s, excluding the terminating NUL, examining at most + * maxlen bytes: the result is min(strlen(s), maxlen) when s is a proper + * string. Unlike strlen, the scan provably stops at maxlen bytes, which is + * why strndup and friends rely on it for untrusted buffers. + * + * See strcpy.c: the NUL scan is exactly the pattern GCC's + * -ftree-loop-distribute-patterns recognizes as strnlen/strlen, so disable + * that transformation to avoid self-recursion. + */ +__attribute__((optimize("no-tree-loop-distribute-patterns"))) size_t +strnlen(const char *s, size_t maxlen) +{ + size_t n = 0; + + while (n < maxlen && s[n] != '\0') + { + n++; + } + + return n; +} diff --git a/src/string/strsignal.c b/src/string/strsignal.c new file mode 100644 index 0000000..fdc3916 --- /dev/null +++ b/src/string/strsignal.c @@ -0,0 +1,136 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * vlibc — strsignal (todo 8). + * + * Maps a signal number to its description string. The table uses the + * x86_64 Linux asm-generic signal numbers (SIGHUP=1 .. SIGSYS=31), which + * are kernel-ABI facts; (todo 28) will become the canonical home + * for the signal-number names, and this table should then reference those + * names instead of the literal numbers. + * + * Known signals return immutable static string literals. Unrecognized + * numbers — including 0, whose text POSIX deliberately leaves unspecified — + * format as "Unknown signal " into one shared static buffer. That is the + * same single-buffer tradeoff as strerror (see src/errno/strerror.c): the + * unknown path is rare diagnostics, there is no per-thread storage beyond + * the errno TCB slot yet, and strsignal must not depend on malloc. Migrate + * to a per-thread buffer once the TCB supports it. + */ + +/* + * One entry per x86_64 asm-generic signal 1..31. The texts are standard + * English descriptions of what each signal means; the wording is vlibc's + * own (POSIX fixes the meanings, not the strings). + */ +static const char *const sigmsg[] = { + [1] = "Hangup", + [2] = "Interrupt", + [3] = "Quit", + [4] = "Illegal instruction", + [5] = "Trace/breakpoint trap", + [6] = "Aborted", + [7] = "Bus error", + [8] = "Floating point exception", + [9] = "Killed", + [10] = "User defined signal 1", + [11] = "Segmentation fault", + [12] = "User defined signal 2", + [13] = "Broken pipe", + [14] = "Alarm clock", + [15] = "Terminated", + [16] = "Stack fault", + [17] = "Child exited", + [18] = "Continued", + [19] = "Stopped (signal)", + [20] = "Stopped", + [21] = "Stopped (tty input)", + [22] = "Stopped (tty output)", + [23] = "Urgent I/O condition", + [24] = "CPU time limit exceeded", + [25] = "File size limit exceeded", + [26] = "Virtual timer expired", + [27] = "Profiling timer expired", + [28] = "Window changed", + [29] = "I/O possible", + [30] = "Power failure", + [31] = "Bad system call", +}; + +/* The table spans exactly 1..31 (index 0 stays NULL: signal 0 is unknown). */ +_Static_assert(sizeof sigmsg / sizeof sigmsg[0] == 32, + "strsignal table must cover signal numbers 1..31"); + +/* Shared storage for the unknown-signal path (see the file-top comment). */ +static char sigbuf[32]; + +/* + * Write "Unknown signal " into dst, truncating to cap bytes and always + * NUL-terminating when cap > 0. Hand-rolled so this file needs no stdio. + */ +static void +format_unknown(int sig, char *dst, size_t cap) // NOLINT(bugprone-easily-swappable-parameters) +{ + static const char prefix[] = "Unknown signal "; + char digits[12]; /* enough for "-2147483648" */ + size_t ndigits; + size_t i; + unsigned long mag; + + if (cap == 0) + { + return; + } + + /* Absolute value as unsigned long; INT_MIN negates safely in long. */ + mag = (unsigned long)(sig < 0 ? -(long)sig : sig); + + /* Digits, least significant first. */ + ndigits = 0; + do + { + digits[ndigits] = (char)('0' + (int)(mag % 10)); + ndigits++; + mag /= 10; + } while (mag != 0); + + i = 0; + while (i + 1 < cap && prefix[i] != '\0') + { + dst[i] = prefix[i]; + i++; + } + if (i + 1 < cap && sig < 0) + { + dst[i] = '-'; + i++; + } + while (i + 1 < cap && ndigits > 0) + { + ndigits--; + dst[i] = digits[ndigits]; + i++; + } + dst[i] = '\0'; +} + +/* + * Return a pointer to a static string describing the signal sig. Never + * returns NULL. Distinct known signals yield distinct strings; signal 0 and + * out-of-range numbers yield "Unknown signal ". + */ +char * +strsignal(int sig) +{ + if (sig > 0 && sig < (int)(sizeof sigmsg / sizeof sigmsg[0])) + { + return (char *)sigmsg[sig]; + } + + format_unknown(sig, sigbuf, sizeof sigbuf); + return sigbuf; +} diff --git a/src/string/strspn.c b/src/string/strspn.c new file mode 100644 index 0000000..2051c62 --- /dev/null +++ b/src/string/strspn.c @@ -0,0 +1,71 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * True when c appears anywhere in set (including as the NUL of set — but set + * is a string, so scanning stops at that NUL, as intended). + */ +static int +str_in_set(const char *set, char c) // NOLINT(bugprone-easily-swappable-parameters) +{ + for (; *set != '\0'; set++) + { + if (*set == c) + { + return 1; + } + } + return 0; +} + +/* + * Return the length of the initial span of s consisting entirely of bytes + * that occur in accept. + */ +size_t +strspn(const char *s, const char *accept) // NOLINT(bugprone-easily-swappable-parameters) +{ + size_t n = 0; + + while (s[n] != '\0' && str_in_set(accept, s[n])) + { + n++; + } + return n; +} + +/* + * Return the length of the initial span of s consisting entirely of bytes + * that do NOT occur in reject. + */ +size_t +strcspn(const char *s, const char *reject) // NOLINT(bugprone-easily-swappable-parameters) +{ + size_t n = 0; + + while (s[n] != '\0' && !str_in_set(reject, s[n])) + { + n++; + } + return n; +} + +/* + * Return a pointer to the first byte in s that also occurs in accept, or + * NULL when none occurs. + */ +char * +strpbrk(const char *s, const char *accept) // NOLINT(bugprone-easily-swappable-parameters) +{ + for (; *s != '\0'; s++) + { + if (str_in_set(accept, *s)) + { + return (char *)s; + } + } + return NULL; +} diff --git a/src/string/strstr.c b/src/string/strstr.c new file mode 100644 index 0000000..b4ace5b --- /dev/null +++ b/src/string/strstr.c @@ -0,0 +1,43 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Return a pointer to the first occurrence of needle in haystack, or NULL + * when absent. An empty needle matches haystack itself. + * + * Naive two-pointer scan: for each position in haystack, compare characters + * until the needle is exhausted (match) or a mismatch occurs (advance). The + * inner loop stops at the NUL terminator of either string, so the comparison + * never reads past the end of haystack when needle is longer than the + * remaining tail. + */ +char * +strstr(const char *haystack, const char *needle) // NOLINT(bugprone-easily-swappable-parameters) +{ + if (*needle == '\0') + { + return (char *)haystack; + } + + for (; *haystack != '\0'; haystack++) + { + const char *h = haystack; + const char *n = needle; + + while (*n != '\0' && *h == *n) + { + h++; + n++; + } + + if (*n == '\0') + { + return (char *)haystack; + } + } + + return NULL; +} diff --git a/src/string/strtok.c b/src/string/strtok.c new file mode 100644 index 0000000..2f5a16e --- /dev/null +++ b/src/string/strtok.c @@ -0,0 +1,73 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +/* + * Shared continuation pointer for the non-reentrant strtok. Plain static + * storage: POSIX requires strtok itself to be neither thread-safe nor + * reentrant; callers needing either use strtok_r. + */ +static char *strtok_next; + +/* + * Split s into tokens delimited by any byte from sep. On the first call s + * names the string; on later calls with s == NULL the search continues in + * the same string. Leading and consecutive delimiters produce no empty + * tokens, and the delimiter bytes in s are overwritten with NUL. Returns + * NULL when no token remains. + */ +char * +strtok(char *restrict s, const char *restrict sep) // NOLINT(bugprone-easily-swappable-parameters) +{ + return strtok_r(s, sep, &strtok_next); +} + +/* + * Reentrant strtok: the continuation pointer lives in *state instead of + * private static storage. When a call finds no token (including the first + * call on an empty or all-delimiter string), *state is set to NULL, so a + * later strtok_r(NULL, sep, state) call returns NULL without further reads. + */ +char * +strtok_r(char *restrict s, const char *restrict sep, char **restrict state) +{ + char *tok; + + if (s == NULL) + { + s = *state; + if (s == NULL) + { + return NULL; + } + } + + /* Skip leading delimiters (this also skips the empty tokens that + consecutive delimiters would otherwise produce). */ + s += strspn(s, sep); + if (*s == '\0') + { + *state = NULL; + return NULL; + } + + tok = s; + + /* Find the end of the token. */ + s += strcspn(s, sep); + if (*s != '\0') + { + /* Replace the delimiter with NUL and continue after it. */ + *s++ = '\0'; + } + else + { + /* The token ran to the end of the string. */ + s = NULL; + } + + *state = s; + return tok; +} diff --git a/tests/test_string.c b/tests/test_string.c new file mode 100644 index 0000000..de8cddd --- /dev/null +++ b/tests/test_string.c @@ -0,0 +1,709 @@ +/* + * vlibc — string.h completion test (todo 8). + * + * Covers the new string search/memory functions end to end: + * + * 1. copy family: strcpy/strncpy (NUL padding), strcat/strncat (always + * NUL-terminated), L2 stpcpy/stpncpy (return the NUL position); + * 2. search family: strchr/strrchr (including the NUL as a match), + * strspn/strcspn/strpbrk, strstr (empty/longer/overlapping needles); + * 3. tokenizer: strtok/strtok_r — empty source yields NULL, consecutive + * delimiters produce NO empty tokens, an all-delimiter string yields + * NULL on the first call, and the delimiter string is never modified; + * 4. collation: strcoll == strcmp and strxfrm (identity transform, + * returns strlen, truncates to n-1 bytes + NUL) — "C" locale only; + * L2 strcoll_l/strxfrm_l ignore the locale argument; + * 5. memory: memchr (incl. negative c), memcmp ordering, L2 memccpy; + * 6. misc: strnlen (stops at maxlen), strncmp (stops at NUL), strdup/ + * strndup round-trips, strsignal (distinct non-null strings; unknown + * numbers format as "Unknown signal "; signal 0 is asserted + * distinct + non-null only, per POSIX). + * + * The expected values were captured against the host glibc as the + * ground-truth oracle for the corpus (see the task-8 evidence log); the + * assertions below encode those golden results inline. Two strxfrm cases + * intentionally assert VLIBc's pinned contract (n-1 bytes + NUL, return + * strlen) rather than glibc's: POSIX leaves the dst contents UNSPECIFIED + * when the return value is >= n. + * + * strdup/strndup allocate through the internal __libc_malloc seam + * (src/internal/malloc.h); under make check the real allocator (todo 7) + * provides it. For the STANDALONE verification of this todo the test links + * a throwaway /tmp allocator stub (never committed) — see the evidence log. + * + * With `-p`, a representative sample of results is printed to stdout for + * eyeball diffing; the assertions always run. + * + * No host libc headers are included (the -Iinclude path would shadow GCC's + * internal headers); all output goes through raw SYS_write via + * . Not yet wired into make check (a later todo). + * + * Standalone build: + * gcc -Iinclude -DVLIBC_LEVEL=2 -std=c23 -Wall -Wextra -pedantic \ + * -o /tmp/t8 tests/test_string.c \ + * src/string/{strcpy,strncpy,strcat,strchr,strspn,strstr,strtok,strcoll,strdup,strnlen,strncmp,memchr,memcmp,strsignal,stpcpy,memccpy}.c \ + * src/string/{strlen,strcmp}.c /tmp/allocstub.c \ + * src/internal/errno.c src/internal/syscall_ret.c + */ + +#include + +#include + +#include "../src/internal/malloc.h" + +/* ---- shared helpers ---- */ + +/* + * Collect up to cap tokens from a fresh copy of s using strtok; the copy is + * consumed (delimiters become NUL), matching POSIX strtok semantics. + */ +static int +tok_collect(char *s, const char *sep, char *out[], int cap) +{ + char *t; + int n = 0; + + t = strtok(s, sep); + while (t != NULL && n < cap) + { + out[n++] = t; + t = strtok(NULL, sep); + } + return n; +} + +/* Same, but through strtok_r with an explicit state. */ +static int +tokr_collect(char *s, const char *sep, char *out[], int cap) +{ + char *t; + char *state = NULL; + int n = 0; + + t = strtok_r(s, sep, &state); + while (t != NULL && n < cap) + { + out[n++] = t; + t = strtok_r(NULL, sep, &state); + } + return n; +} + +/* + * GCC's -Wstringop-truncation/-Wstringop-overread fire when the source + * length is statically visible at a bounded-copy call site. The truncation + * and oversized-n corpus cases below are exactly what those diagnostics + * guard against — intentionally — so they go through noipa wrappers + * (noipa, not just noinline: GCC's constprop clones ignore noinline at + * -O2 and re-expose the call sites). + */ +static __attribute__((noipa)) char * +strncpy_opaque(char *dst, const char *src, size_t n) +{ + return strncpy(dst, src, n); +} + +static __attribute__((noipa)) char * +strncat_opaque(char *dst, const char *src, size_t n) +{ + return strncat(dst, src, n); +} + +static __attribute__((noipa)) char * +strndup_opaque(const char *s, size_t n) +{ + return strndup(s, n); +} + +/* ---- test functions (one per checked behavior) ---- */ + +static int +test_strcpy(void) +{ + char buf[16]; + + TEST_ASSERT_TRUE(strcpy(buf, "hello") == buf); + TEST_ASSERT_STREQ(buf, "hello"); + TEST_ASSERT_TRUE(strcpy(buf, "") == buf); + TEST_ASSERT_STREQ(buf, ""); + return 0; +} + +static int +test_strncpy(void) +{ + char buf[8]; + + /* Shorter src: NUL-padded to n. */ + memset(buf, 0x41, sizeof buf); + TEST_ASSERT_TRUE(strncpy(buf, "hi", 5) == buf); + TEST_ASSERT_STREQ(buf, "hi"); + TEST_ASSERT_EQ(buf[2], '\0'); + TEST_ASSERT_EQ(buf[3], '\0'); + TEST_ASSERT_EQ(buf[4], '\0'); + TEST_ASSERT_EQ(buf[5], 'A'); /* untouched past n */ + + /* Src n bytes or longer: no NUL written within n. */ + memset(buf, 0x41, sizeof buf); + TEST_ASSERT_TRUE(strncpy_opaque(buf, "hello", 3) == buf); + TEST_ASSERT_EQ(buf[0], 'h'); + TEST_ASSERT_EQ(buf[1], 'e'); + TEST_ASSERT_EQ(buf[2], 'l'); + TEST_ASSERT_EQ(buf[3], 'A'); /* not NUL-terminated */ + + /* n == 0: nothing written. */ + memset(buf, 0x41, sizeof buf); + strncpy_opaque(buf, "hello", 0); + TEST_ASSERT_EQ(buf[0], 'A'); + return 0; +} + +static int +test_strcat(void) +{ + char buf[16]; + + strcpy(buf, "hello"); + TEST_ASSERT_TRUE(strcat(buf, " world") == buf); + TEST_ASSERT_STREQ(buf, "hello world"); + strcpy(buf, ""); + strcat(buf, "x"); + TEST_ASSERT_STREQ(buf, "x"); + return 0; +} + +static int +test_strncat(void) +{ + char buf[16]; + + strcpy(buf, "abc"); + TEST_ASSERT_TRUE(strncat_opaque(buf, "defgh", 3) == buf); + TEST_ASSERT_STREQ(buf, "abcdef"); + + strcpy(buf, "abc"); + strncat(buf, "defgh", 10); + TEST_ASSERT_STREQ(buf, "abcdefgh"); + + strcpy(buf, "abc"); + strncat_opaque(buf, "xyz", 0); + TEST_ASSERT_STREQ(buf, "abc"); + return 0; +} + +static int +test_strchr(void) +{ + const char *s = "hello"; + + TEST_ASSERT_EQ(strchr(s, 'l') - s, 2); + TEST_ASSERT_EQ(strchr(s, 'h') - s, 0); + TEST_ASSERT_NULL(strchr(s, 'z')); + TEST_ASSERT_EQ(strchr(s, '\0') - s, 5); + return 0; +} + +static int +test_strrchr(void) +{ + const char *s = "hello"; + + TEST_ASSERT_EQ(strrchr(s, 'l') - s, 3); + TEST_ASSERT_EQ(strrchr(s, 'h') - s, 0); + TEST_ASSERT_NULL(strrchr(s, 'z')); + TEST_ASSERT_EQ(strrchr(s, '\0') - s, 5); + return 0; +} + +static int +test_strspn(void) +{ + TEST_ASSERT_EQ(strspn("abcba", "abc"), 5); + TEST_ASSERT_EQ(strspn("abcba", "ab"), 2); + TEST_ASSERT_EQ(strspn("abcba", "z"), 0); + TEST_ASSERT_EQ(strspn("", "ab"), 0); + return 0; +} + +static int +test_strcspn(void) +{ + TEST_ASSERT_EQ(strcspn("abcba", "b"), 1); + TEST_ASSERT_EQ(strcspn("abcba", "z"), 5); + TEST_ASSERT_EQ(strcspn("abcba", "a"), 0); + TEST_ASSERT_EQ(strcspn("", "ab"), 0); + return 0; +} + +static int +test_strpbrk(void) +{ + const char *s = "hello"; + + TEST_ASSERT_EQ(strpbrk(s, "aeiou") - s, 1); + TEST_ASSERT_EQ(strpbrk(s, "h") - s, 0); + TEST_ASSERT_NULL(strpbrk(s, "xyz")); + TEST_ASSERT_NULL(strpbrk("", "x")); + return 0; +} + +static int +test_strstr(void) +{ + const char *h = "hello world"; + + TEST_ASSERT_EQ(strstr(h, "world") - h, 6); + TEST_ASSERT_EQ(strstr(h, "hello") - h, 0); + TEST_ASSERT_EQ(strstr(h, "o") - h, 4); + TEST_ASSERT_NULL(strstr(h, "helloo")); + TEST_ASSERT_NULL(strstr("hi", "hello")); /* needle longer than haystack */ + TEST_ASSERT_TRUE(strstr(h, "") == h); /* empty needle matches haystack */ + TEST_ASSERT_EQ(strstr("aaaa", "aaa") - "aaaa", 0); /* overlapping */ + TEST_ASSERT_EQ(strstr("banana", "nan") - "banana", 2); + TEST_ASSERT_EQ(strstr("abc", "bc") - "abc", 1); + return 0; +} + +static int +test_strtok(void) +{ + char buf[32]; + char *out[8]; + int n; + + /* Empty source: first call yields NULL (no tokens). */ + strcpy(buf, ""); + TEST_ASSERT_NULL(strtok(buf, " ")); + + /* Consecutive delimiters produce NO empty tokens. */ + strcpy(buf, "a,,b"); + n = tok_collect(buf, ",", out, 8); + TEST_ASSERT_EQ(n, 2); + TEST_ASSERT_STREQ(out[0], "a"); + TEST_ASSERT_STREQ(out[1], "b"); + + /* All delimiters: first call yields NULL. */ + strcpy(buf, ",,,"); + TEST_ASSERT_NULL(strtok(buf, ",")); + + /* Leading + trailing delimiters are skipped. */ + strcpy(buf, "xxhelloxx"); + n = tok_collect(buf, "x", out, 8); + TEST_ASSERT_EQ(n, 1); + TEST_ASSERT_STREQ(out[0], "hello"); + + /* No delimiter present: the whole string is one token. */ + strcpy(buf, "hello"); + n = tok_collect(buf, " ", out, 8); + TEST_ASSERT_EQ(n, 1); + TEST_ASSERT_STREQ(out[0], "hello"); + + /* Multiple whitespace runs collapse to the same tokens. */ + strcpy(buf, "a b c"); + n = tok_collect(buf, " ", out, 8); + TEST_ASSERT_EQ(n, 3); + TEST_ASSERT_STREQ(out[0], "a"); + TEST_ASSERT_STREQ(out[1], "b"); + TEST_ASSERT_STREQ(out[2], "c"); + + /* The delimiter string is never modified. */ + { + static char sep[] = ",;"; + char *sep_before = sep; + + strcpy(buf, "a,b;c"); + (void)tok_collect(buf, sep, out, 8); + TEST_ASSERT_TRUE(sep == sep_before); + TEST_ASSERT_EQ(sep[0], ','); + TEST_ASSERT_EQ(sep[1], ';'); + } + return 0; +} + +static int +test_strtok_r(void) +{ + char buf[32]; + char *out[8]; + int n; + + strcpy(buf, "a,,b"); + n = tokr_collect(buf, ",", out, 8); + TEST_ASSERT_EQ(n, 2); + TEST_ASSERT_STREQ(out[0], "a"); + TEST_ASSERT_STREQ(out[1], "b"); + + strcpy(buf, ""); + TEST_ASSERT_NULL(strtok_r(buf, " ", &(char *){NULL})); + + /* Two interleaved scans keep independent state. */ + { + char s1[16]; + char s2[16]; + char *state1 = NULL; + char *state2 = NULL; + char *t1; + char *t2; + + strcpy(s1, "one two"); + strcpy(s2, "red green blue"); + + t1 = strtok_r(s1, " ", &state1); + t2 = strtok_r(s2, " ", &state2); + TEST_ASSERT_STREQ(t1, "one"); + TEST_ASSERT_STREQ(t2, "red"); + t1 = strtok_r(NULL, " ", &state1); + TEST_ASSERT_STREQ(t1, "two"); + t2 = strtok_r(NULL, " ", &state2); + TEST_ASSERT_STREQ(t2, "green"); + t1 = strtok_r(NULL, " ", &state1); + TEST_ASSERT_NULL(t1); + t2 = strtok_r(NULL, " ", &state2); + TEST_ASSERT_STREQ(t2, "blue"); + } + return 0; +} + +static int +test_strcoll(void) +{ + TEST_ASSERT_EQ(strcoll("apple", "banana"), strcmp("apple", "banana")); + TEST_ASSERT_EQ(strcoll("apple", "banana") < 0, 1); + TEST_ASSERT_EQ(strcoll("b", "a") > 0, 1); + TEST_ASSERT_EQ(strcoll("same", "same"), 0); + TEST_ASSERT_EQ(strcoll("A", "a"), strcmp("A", "a")); + TEST_ASSERT_EQ(strcoll("", ""), 0); + return 0; +} + +static int +test_strxfrm(void) +{ + char xf[16]; + + /* Identity transform, returns strlen(src). */ + TEST_ASSERT_EQ(strxfrm(xf, "hello", sizeof xf), 5); + TEST_ASSERT_STREQ(xf, "hello"); + + /* Truncation: at most n-1 bytes + NUL, return still 5. */ + TEST_ASSERT_EQ(strxfrm(xf, "hello", 4), 5); + TEST_ASSERT_STREQ(xf, "hel"); + + /* n == 0: nothing written, return still 5. */ + memset(xf, 0x41, sizeof xf); + TEST_ASSERT_EQ(strxfrm(xf, "hello", 0), 5); + TEST_ASSERT_EQ(xf[0], 'A'); + + /* n == 1: only the NUL. */ + TEST_ASSERT_EQ(strxfrm(xf, "hello", 1), 5); + TEST_ASSERT_EQ(xf[0], '\0'); + + TEST_ASSERT_EQ(strxfrm(xf, "", sizeof xf), 0); + TEST_ASSERT_STREQ(xf, ""); + return 0; +} + +static int +test_strdup(void) +{ + char src[] = "hello"; + char *p = strdup(src); + + TEST_ASSERT_TRUE(p != NULL); + TEST_ASSERT_STREQ(p, "hello"); + TEST_ASSERT_TRUE(p != src); /* a real copy, not the source array */ + __libc_free(p); + + p = strdup(""); + TEST_ASSERT_TRUE(p != NULL); + TEST_ASSERT_STREQ(p, ""); + __libc_free(p); + return 0; +} + +static int +test_strndup(void) +{ + char *p = strndup("abcdef", 3); + + TEST_ASSERT_TRUE(p != NULL); + TEST_ASSERT_STREQ(p, "abc"); + __libc_free(p); + + p = strndup_opaque("ab", 10); /* n beyond the NUL: whole string */ + TEST_ASSERT_TRUE(p != NULL); + TEST_ASSERT_STREQ(p, "ab"); + __libc_free(p); + + p = strndup("xyz", 0); /* n == 0: empty string */ + TEST_ASSERT_TRUE(p != NULL); + TEST_ASSERT_STREQ(p, ""); + __libc_free(p); + + p = strndup("", 0); + TEST_ASSERT_TRUE(p != NULL); + TEST_ASSERT_STREQ(p, ""); + __libc_free(p); + return 0; +} + +static int +test_strnlen(void) +{ + TEST_ASSERT_EQ(strnlen("abc", 2), 2); + TEST_ASSERT_EQ(strnlen("abc", 10), 3); + TEST_ASSERT_EQ(strnlen("abc", 0), 0); + TEST_ASSERT_EQ(strnlen("", 5), 0); + return 0; +} + +static int +test_strncmp(void) +{ + TEST_ASSERT_EQ(strncmp("abc", "abc", 3), 0); + TEST_ASSERT_EQ(strncmp("ab", "abc", 2), 0); + TEST_ASSERT_EQ(strncmp("ab", "ac", 1), 0); + TEST_ASSERT_EQ(strncmp("abc", "abd", 3) < 0, 1); + TEST_ASSERT_EQ(strncmp("abd", "abc", 3) > 0, 1); + TEST_ASSERT_EQ(strncmp("x", "y", 0), 0); /* n == 0 */ + /* Stops at the first NUL even when n is larger. */ + TEST_ASSERT_EQ(strncmp("a\0b", "a\0c", 5), 0); + TEST_ASSERT_EQ(strncmp("a\0b", "a", 5), 0); + return 0; +} + +static int +test_memchr(void) +{ + const char *s = "hello"; + unsigned char high[] = {0x41, 0xff, 0x00}; + + TEST_ASSERT_EQ((char *)memchr(s, 'l', 5) - s, 2); + TEST_ASSERT_NULL(memchr(s, 'z', 5)); + TEST_ASSERT_NULL(memchr(s, 'h', 0)); /* n == 0 */ + TEST_ASSERT_EQ((char *)memchr(s, '\0', 6) - s, 5); + /* Negative c searches for the matching high byte. */ + TEST_ASSERT_EQ((char *)memchr(high, 0xff, 3) - (char *)high, 1); + return 0; +} + +static int +test_memcmp(void) +{ + unsigned char hi[] = {0xff, 0x00}; + unsigned char lo[] = {0x7f, 0x00}; + + TEST_ASSERT_EQ(memcmp("abc", "abc", 3), 0); + TEST_ASSERT_EQ(memcmp("abc", "abd", 3) < 0, 1); + TEST_ASSERT_EQ(memcmp("abd", "abc", 3) > 0, 1); + TEST_ASSERT_EQ(memcmp("x", "y", 0), 0); /* n == 0 */ + /* Bytes compare as unsigned char: 0xff > 0x7f. */ + TEST_ASSERT_EQ(memcmp(hi, lo, 1) > 0, 1); + TEST_ASSERT_EQ(memcmp(lo, hi, 1) < 0, 1); + /* Does not stop at NUL bytes. */ + TEST_ASSERT_EQ(memcmp("a\0b", "a\0c", 3) < 0, 1); + return 0; +} + +static int +test_strsignal(void) +{ + /* Known signals: pinned texts (vlibc's own table, x86_64 numbers). */ + TEST_ASSERT_STREQ(strsignal(2), "Interrupt"); + TEST_ASSERT_STREQ(strsignal(11), "Segmentation fault"); + TEST_ASSERT_STREQ(strsignal(1), "Hangup"); + TEST_ASSERT_STREQ(strsignal(9), "Killed"); + TEST_ASSERT_STREQ(strsignal(31), "Bad system call"); + + /* Distinct strings for distinct known signals. */ + TEST_ASSERT_TRUE(strsignal(2) != strsignal(11)); + TEST_ASSERT_TRUE(strsignal(1) != strsignal(2)); + + /* Signal 0: non-null and distinct, text deliberately not pinned. */ + TEST_ASSERT_TRUE(strsignal(0) != NULL); + TEST_ASSERT_TRUE(strsignal(0) != strsignal(1)); + TEST_ASSERT_TRUE(strsignal(0) != strsignal(2)); + + /* Unknown numbers: formatted, non-null, distinct. */ + TEST_ASSERT_STREQ(strsignal(999), "Unknown signal 999"); + TEST_ASSERT_STREQ(strsignal(32), "Unknown signal 32"); + TEST_ASSERT_TRUE(strsignal(-1) != NULL); + TEST_ASSERT_STREQ(strsignal(-1), "Unknown signal -1"); + return 0; +} + +static int +test_stpcpy_stpncpy(void) +{ + char buf[8]; + char *end; + + end = stpcpy(buf, "abc"); + TEST_ASSERT_EQ(end - buf, 3); + TEST_ASSERT_STREQ(buf, "abc"); + TEST_ASSERT_EQ(*end, '\0'); + + end = stpcpy(buf, ""); + TEST_ASSERT_EQ(end - buf, 0); + TEST_ASSERT_EQ(buf[0], '\0'); + + /* Short src: NUL-padded; return points at the first NUL. */ + end = stpncpy(buf, "abc", 8); + TEST_ASSERT_EQ(end - buf, 3); + TEST_ASSERT_STREQ(buf, "abc"); + + /* Src >= n: no NUL written; return is dst + n. */ + memset(buf, 'X', sizeof buf); + end = stpncpy(buf, "abcdefgh", 4); + TEST_ASSERT_EQ(end - buf, 4); + TEST_ASSERT_EQ(buf[0], 'a'); + TEST_ASSERT_EQ(buf[3], 'd'); + TEST_ASSERT_EQ(buf[4], 'X'); + + /* n == 0: return dst, nothing written. */ + memset(buf, 'X', sizeof buf); + end = stpncpy(buf, "abc", 0); + TEST_ASSERT_EQ(end - buf, 0); + TEST_ASSERT_EQ(buf[0], 'X'); + return 0; +} + +static int +test_memccpy(void) +{ + unsigned char buf[16]; + void *p; + + p = memccpy(buf, "hello world", ' ', 20); + TEST_ASSERT_EQ((char *)p - (char *)buf, 6); /* one past the ' ' */ + TEST_ASSERT_EQ(buf[5], ' '); + + /* c never appears within n: NULL, full copy made. */ + p = memccpy(buf, "hello", 'z', 20); + TEST_ASSERT_NULL(p); + TEST_ASSERT_EQ(buf[4], 'o'); + + /* c exists but beyond n: NULL. */ + p = memccpy(buf, "hello", 'o', 3); + TEST_ASSERT_NULL(p); + + /* n == 0: NULL, nothing written. */ + memset(buf, 'X', sizeof buf); + p = memccpy(buf, "hello", 'h', 0); + TEST_ASSERT_NULL(p); + TEST_ASSERT_EQ(buf[0], 'X'); + return 0; +} + +static int +test_strcoll_l(void) +{ + char xf[16]; + + /* The locale argument is ignored ("C" locale behavior). */ + TEST_ASSERT_EQ(strcoll_l("a", "b", NULL), strcoll("a", "b")); + TEST_ASSERT_EQ(strcoll_l("a", "b", NULL) < 0, 1); + TEST_ASSERT_EQ(strcoll_l("x", "x", NULL), 0); + TEST_ASSERT_EQ(strxfrm_l(xf, "hi", sizeof xf, NULL), strxfrm(xf, "hi", sizeof xf)); + TEST_ASSERT_STREQ(xf, "hi"); + return 0; +} + +/* ---- register + runner ---- */ + +static const struct vlibc_test tests[] = { + {"strcpy", test_strcpy}, {"strncpy", test_strncpy}, + {"strcat", test_strcat}, {"strncat", test_strncat}, + {"strchr", test_strchr}, {"strrchr", test_strrchr}, + {"strspn", test_strspn}, {"strcspn", test_strcspn}, + {"strpbrk", test_strpbrk}, {"strstr", test_strstr}, + {"strtok", test_strtok}, {"strtok_r", test_strtok_r}, + {"strcoll", test_strcoll}, {"strxfrm", test_strxfrm}, + {"strdup", test_strdup}, {"strndup", test_strndup}, + {"strnlen", test_strnlen}, {"strncmp", test_strncmp}, + {"memchr", test_memchr}, {"memcmp", test_memcmp}, + {"strsignal", test_strsignal}, {"stpcpy/stpncpy", test_stpcpy_stpncpy}, + {"memccpy", test_memccpy}, {"strcoll_l/strxfrm_l", test_strcoll_l}, +}; + +/* -p mode: print a representative sample for eyeball diffing. */ +static void +print_sample(void) +{ + char buf[32]; + char *out[8]; + int n; + char *t; + + vlibc_test_say(1, "strtok(a,,b, ,)= "); + strcpy(buf, "a,,b"); + n = tok_collect(buf, ",", out, 8); + for (int i = 0; i < n; i++) + { + vlibc_test_say(1, "["); + vlibc_test_say(1, out[i]); + vlibc_test_say(1, "]"); + } + vlibc_test_say(1, "\nstrxfrm(hello,n=4)=len "); + vlibc_test_say_dec(1, (unsigned long)strxfrm(buf, "hello", 4)); + vlibc_test_say(1, " dst=\""); + vlibc_test_say(1, buf); + vlibc_test_say(1, "\"\nstrsignal(2)=\""); + vlibc_test_say(1, strsignal(2)); + vlibc_test_say(1, "\" strsignal(0)=\""); + vlibc_test_say(1, strsignal(0)); + vlibc_test_say(1, "\" strsignal(999)=\""); + vlibc_test_say(1, strsignal(999)); + vlibc_test_say(1, "\"\n"); + t = strdup("dup-ok"); + vlibc_test_say(1, "strdup(dup-ok)=\""); + vlibc_test_say(1, t); + vlibc_test_say(1, "\"\n"); + __libc_free(t); +} + +/* + * Own main (not TEST_MAIN): supports the -p print mode. Everything else + * follows the TEST_MAIN contract — same output shape, 0 on all-pass. + */ +int +main(int argc, char **argv) +{ + const size_t count = sizeof tests / sizeof tests[0]; + size_t passed = 0; + size_t i; + + if (argc > 1 && strcmp(argv[1], "-p") == 0) + { + print_sample(); + } + + for (i = 0; i < count; i++) + { + int before = vlibc_test_failures; + + vlibc_test_say(1, "RUN "); + vlibc_test_say(1, tests[i].name); + vlibc_test_say(1, ": "); + if (tests[i].run() == 0 && vlibc_test_failures == before) + { + vlibc_test_say(1, "PASS\n"); + passed++; + } + else + { + vlibc_test_say(1, "FAIL\n"); + } + } + + vlibc_test_say(1, "SUMMARY: "); + vlibc_test_say_dec(1, (unsigned long)passed); + vlibc_test_say(1, "/"); + vlibc_test_say_dec(1, (unsigned long)count); + vlibc_test_say(1, " passed, "); + vlibc_test_say_dec(1, (unsigned long)vlibc_test_failures); + vlibc_test_say(1, " assertion failure(s)\n"); + + return passed == count ? 0 : 1; +}