feat(string): complete string.h + strdup/memccpy

This commit is contained in:
2026-09-03 20:05:49 -04:00
parent 20f514d91c
commit 06d2ec7c34
19 changed files with 1860 additions and 2 deletions
+30
View File
@@ -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 <stddef.h>
#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 */
+40
View File
@@ -0,0 +1,40 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#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) */
+34
View File
@@ -0,0 +1,34 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+34
View File
@@ -0,0 +1,34 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+69
View File
@@ -0,0 +1,69 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#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) */
+65
View File
@@ -0,0 +1,65 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+52
View File
@@ -0,0 +1,52 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
}
}
+79
View File
@@ -0,0 +1,79 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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 <locale.h>, 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) */
+31
View File
@@ -0,0 +1,31 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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++;
}
}
+69
View File
@@ -0,0 +1,69 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
#include <errno.h>
#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;
}
+36
View File
@@ -0,0 +1,36 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+35
View File
@@ -0,0 +1,35 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+28
View File
@@ -0,0 +1,28 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+136
View File
@@ -0,0 +1,136 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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; <signal.h> (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 <N>" 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 <sig>" 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 <sig>".
*/
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;
}
+71
View File
@@ -0,0 +1,71 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+43
View File
@@ -0,0 +1,43 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}
+73
View File
@@ -0,0 +1,73 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <string.h>
/*
* 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;
}