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