diff --git a/include/strings.h b/include/strings.h new file mode 100644 index 0000000..294e983 --- /dev/null +++ b/include/strings.h @@ -0,0 +1,133 @@ +#ifndef VLIBC_STRINGS_H +#define VLIBC_STRINGS_H + +/* + * vlibc — . + * + * BSD/XSI legacy string functions, gated by the active compatibility + * profile (see include/vlibc/features.h). This ENTIRE header is a legacy + * extension: nothing it declares is POSIX.1-2008 base, so at level 1 + * (onlyposix) it is empty. POSIX mandates that (BSD legacy) + * and (ISO C) remain separate headers; the two never share a + * declaration, so including both can never conflict. + * + * strcasecmp / strncasecmp — XSI case-insensitive comparisons (ASCII + * 'A'..'Z'/'a'..'z' fold only, byte-wise); + * ffs / ffsl / ffsll — XSI find-first-set-bit, 1-based; + * bcmp / bcopy / bzero — BSD legacy byte operations (bcopy takes + * (src, dst) — the arguments are REVERSED + * relative to memcpy/memmove); + * index / rindex — BSD legacy names for strchr / strrchr. + * + * This header includes itself, so the gate below always + * sees the configured VLIBC_LEVEL even when the caller included no vlibc + * header first, and for size_t. + */ + +#include + +#if VLIBC_LEVEL_GE(2) + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Compare s1 and s2 byte-wise, ignoring case: each byte is read as + * unsigned char and an ASCII 'A'..'Z' is folded to 'a'..'z' before the + * comparison; bytes at or above 0x80 pass through unmodified. Return + * negative, zero, or positive when s1 is less than, equal to, or greater + * than s2. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) int +strcasecmp(const char *s1, const char *s2); + +/* + * Compare at most n bytes of s1 and s2 as strcasecmp does, stopping early + * at the first difference or the first NUL; return negative, zero, or + * positive like strcasecmp. The NUL terminator of either string ends the + * comparison even when n is larger. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) int +strncasecmp(const char *s1, const char *s2, size_t n); + +/* + * Return the 1-based index of the least significant set bit of i, or 0 + * when i has no set bit: ffs(0) == 0, ffs(1) == 1, ffs(8) == 4, + * ffs(INT_MIN) == 32. + * const: the result depends only on i. + */ +__attribute__((const)) int +ffs(int i); + +/* + * Same as ffs for long: ffsl(0) == 0, ffsl(1L << 40) == 41, + * ffsl(LONG_MIN) == 64 (long is 64-bit on x86_64). + * const: the result depends only on i. + */ +__attribute__((const)) int +ffsl(long i); + +/* + * Same as ffs for long long: ffsll(0) == 0, ffsll(LLONG_MIN) == 64. + * const: the result depends only on i. + */ +__attribute__((const)) int +ffsll(long long i); + +/* + * Compare the first n bytes of s1 and s2 as unsigned char, like memcmp; + * unlike strcmp a NUL byte does not end the comparison. Return 0 when the + * n bytes are equal, nonzero otherwise. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) int +bcmp(const void *s1, const void *s2, size_t n); + +/* + * Copy n bytes from src to dst. The regions may overlap and the copy + * behaves like memmove. NOTE the argument order: source first, destination + * second — the reverse of memcpy/memmove. + * No intent attribute: it writes memory. + */ +void +bcopy(const void *src, void *dst, size_t n); + +/* + * Fill n bytes at s with zero. + * No intent attribute: it writes memory. + */ +void +bzero(void *s, size_t n); + +/* + * Return a pointer to the first occurrence of c (converted to unsigned + * char) in s, or NULL when absent. The terminating NUL is part of the + * string, so index(s, '\0') returns a pointer to it. Legacy name for + * strchr. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) char * +index(const char *s, int c); + +/* + * Return a pointer to the last occurrence of c (converted to unsigned + * char) in s, or NULL when absent. The terminating NUL is part of the + * string, so rindex(s, '\0') returns a pointer to it. Legacy name for + * strrchr. + * pure: reads memory, no side effects. + */ +__attribute__((pure)) char * +rindex(const char *s, int c); + +#ifdef __cplusplus +} +#endif + +#endif /* VLIBC_LEVEL_GE(2) */ + +#endif /* VLIBC_STRINGS_H */ diff --git a/src/string/strings_impl.c b/src/string/strings_impl.c new file mode 100644 index 0000000..d1fb61a --- /dev/null +++ b/src/string/strings_impl.c @@ -0,0 +1,202 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#if VLIBC_LEVEL_GE(2) + +/* + * vlibc — legacy functions (todo 10). + * + * The whole file self-gates at VLIBC_LEVEL >= 2 (the build wiring pass may + * compile it unconditionally): at level 1 this translation unit produces + * nothing, because every function here is XSI/BSD legacy and none is + * POSIX.1-2008 base. + * + * bcopy/bzero/bcmp delegate to the project's own memmove/memset/memcmp + * (satisfied from the archive at link time) — bcopy MUST NOT be a bare + * memcpy alias, because its contract includes overlapping regions. + * index/rindex are written as direct byte scans (own code, not strchr + * wrappers) so their documented unsigned char conversion is explicit. + */ + +/* + * Fold an ASCII uppercase letter to lowercase; every other byte passes + * through unchanged. Kept local and locale-free: the BSD comparisons must + * never depend on ctype tables or the active locale. + */ +static inline unsigned char +fold_ascii_byte(unsigned char c) +{ + if (c >= 'A' && c <= 'Z') + { + return (unsigned char)(c + ('a' - 'A')); + } + return c; +} + +/* + * Compare s1 and s2 byte-wise ignoring case. Equal-folded runs are skipped + * with a raw-byte fast path; the first differing byte pair is compared + * folded, so "A" and "a" order as equal and the returned sign is that of + * the folded difference. The scan stops at the first NUL in either string. + */ +int +strcasecmp(const char *s1, const char *s2) +{ + const unsigned char *l = (const unsigned char *)s1; + const unsigned char *r = (const unsigned char *)s2; + + for (; *l == *r; l++, r++) + { + if (*l == '\0') + { + return 0; + } + } + + return (int)fold_ascii_byte(*l) - (int)fold_ascii_byte(*r); +} + +/* + * Compare at most n bytes of s1 and s2 as strcasecmp does, stopping early + * at the first folded difference or the first NUL. When the comparison + * ends at a NUL, the shorter (equal-prefix) string sorts first. + */ +int +strncasecmp(const char *s1, const char *s2, size_t n) +{ + const unsigned char *l = (const unsigned char *)s1; + const unsigned char *r = (const unsigned char *)s2; + + while (n != 0 && *l != '\0' && *r != '\0') + { + const unsigned char fl = fold_ascii_byte(*l); + const unsigned char fr = fold_ascii_byte(*r); + + if (fl != fr) + { + return (int)fl - (int)fr; + } + l++; + r++; + n--; + } + + /* n bytes compared: equal so far. */ + if (n == 0) + { + return 0; + } + + /* A NUL ended the scan: the one that is NUL here is the shorter. */ + return (int)*l - (int)*r; +} + +/* + * Return the 1-based index of the least significant set bit of i, or 0 + * when i is 0. The zero case is guarded because __builtin_ctz is + * undefined for a zero argument. + */ +int +ffs(int i) +{ + return i == 0 ? 0 : __builtin_ctz((unsigned int)i) + 1; +} + +int +ffsl(long i) +{ + return i == 0 ? 0 : __builtin_ctzl((unsigned long)i) + 1; +} + +int +ffsll(long long i) +{ + return i == 0 ? 0 : __builtin_ctzll((unsigned long long)i) + 1; +} + +/* + * Copy n bytes from src to dst with memmove semantics: the regions may + * overlap and the copy behaves as if through a temporary. The argument + * order (src, dst) is the reverse of memmove's — kept as such because it + * is the BSD contract. + */ +void +bcopy(const void *src, void *dst, size_t n) +{ + (void)memmove(dst, src, n); +} + +/* + * Fill n bytes at s with zero. + */ +void +bzero(void *s, size_t n) +{ + (void)memset(s, 0, n); +} + +/* + * Compare the first n bytes of s1 and s2 as unsigned char, like memcmp; + * return 0 when equal, nonzero otherwise. A NUL byte does not end the + * comparison. + */ +int +bcmp(const void *s1, const void *s2, size_t n) +{ + return memcmp(s1, s2, n); +} + +/* + * Return a pointer to the first occurrence of c (converted to unsigned + * char) in s, or NULL when absent. The terminating NUL is part of the + * string, so index(s, '\0') returns a pointer to it. Legacy name for + * strchr. + */ +char * +index(const char *s, int c) +{ + const unsigned char cc = (unsigned char)c; + + for (;; s++) + { + if ((unsigned char)*s == cc) + { + return (char *)s; + } + if (*s == '\0') + { + return NULL; + } + } +} + +/* + * Return a pointer to the last occurrence of c (converted to unsigned + * char) in s, or NULL when absent. The terminating NUL is part of the + * string, so rindex(s, '\0') returns a pointer to it. Legacy name for + * strrchr. + */ +char * +rindex(const char *s, int c) +{ + const unsigned char cc = (unsigned char)c; + const char *last = NULL; + + for (;; s++) + { + if ((unsigned char)*s == cc) + { + last = s; + } + if (*s == '\0') + { + return (char *)last; + } + } +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/tests/test_strings.c b/tests/test_strings.c new file mode 100644 index 0000000..5b785f2 --- /dev/null +++ b/tests/test_strings.c @@ -0,0 +1,297 @@ +/* + * vlibc — strings.h legacy functions test (todo 10). + * + * Covers the whole surface end to end: + * + * 1. strcasecmp: ASCII case-insensitive compare ("AbC" vs "aBc" == 0), + * ordering sign, empty strings, and high bytes (>= 0x80) compared + * unmodified — no locale folding; + * 2. strncasecmp: the n bound (differences beyond n are invisible), + * n == 0, and NUL-terminated early stop; + * 3. ffs/ffsl/ffsll: ffs(0) == 0, ffs(8) == 4, ffs(INT_MIN) == 32, + * ffsl(LONG_MIN) == 64, ffsll(LLONG_MIN) == 64, plus low-bit spots; + * 4. bcopy: (src, dst) argument order (reversed vs memcpy), and the + * discriminating overlap case dst == src + 1 — a forward-only copy + * would smear the source byte and produce "aaaaaa" instead of + * "aabcde"; + * 5. bzero: zeroes the first n bytes and nothing beyond; n == 0 is a + * no-op; + * 6. bcmp: equal/differ, does not stop at NUL, unsigned byte order; + * 7. index/rindex: first/last occurrence, the NUL counts as a match, + * c converts through unsigned char, NULL when absent. + * + * With `-f`, only the plan's failure scenario runs: index("abc", 'z') and + * rindex("abc", 'z') must BOTH be NULL (character absent); a non-NULL + * result is the defect. Exits 0 when both are NULL as expected. + * + * 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 (the build + * wiring todo owns that). + * + * The whole body mirrors the header's gate: every function is + * level 2, so at level 1 this TU compiles to a no-op runner (the test_ctype + * precedent). Compile the real test with -DVLIBC_LEVEL=2. + * + * Standalone build: + * gcc -Iinclude -DVLIBC_LEVEL=2 -std=c23 -Wall -Wextra -pedantic \ + * -o /tmp/t10 tests/test_strings.c src/string/strings_impl.c \ + * src/string/{memmove,memset,memcmp}.c + */ + +#include + +#include + +#include + +#if VLIBC_LEVEL_GE(2) + +/* ---- test functions (one per checked behavior) ---- */ + +static int +test_strcasecmp(void) +{ + static const unsigned char hi[] = {0xe1, 0x00}; + static const unsigned char lo[] = {0x61, 0x00}; + + TEST_ASSERT_EQ(strcasecmp("AbC", "aBc"), 0); + TEST_ASSERT_EQ(strcasecmp("", ""), 0); + TEST_ASSERT_EQ(strcasecmp("cafe", "CAFE"), 0); + TEST_ASSERT_TRUE(strcasecmp("abc", "abd") < 0); + TEST_ASSERT_TRUE(strcasecmp("abd", "abc") > 0); + TEST_ASSERT_TRUE(strcasecmp("", "a") < 0); + TEST_ASSERT_TRUE(strcasecmp("a", "") > 0); + TEST_ASSERT_TRUE(strcasecmp("Z", "a") > 0); /* folds 'Z' to 'z' */ + TEST_ASSERT_TRUE(strcasecmp("A", "z") < 0); + /* Bytes >= 0x80 pass through unmodified and sort above ASCII. */ + TEST_ASSERT_EQ(strcasecmp((const char *)hi, (const char *)hi), 0); + TEST_ASSERT_TRUE(strcasecmp((const char *)hi, (const char *)lo) > 0); + return 0; +} + +static int +test_strncasecmp(void) +{ + TEST_ASSERT_EQ(strncasecmp("AbCd", "aBcD", 4), 0); + TEST_ASSERT_EQ(strncasecmp("abcX", "abcY", 3), 0); /* diff beyond n */ + TEST_ASSERT_TRUE(strncasecmp("abcX", "abcY", 4) < 0); + TEST_ASSERT_EQ(strncasecmp("x", "y", 0), 0); /* n == 0 */ + TEST_ASSERT_EQ(strncasecmp("", "", 5), 0); + TEST_ASSERT_EQ(strncasecmp("a\0b", "a\0c", 3), 0); /* stops at NUL */ + TEST_ASSERT_TRUE(strncasecmp("abc", "abcd", 4) < 0); /* shorter lhs */ + TEST_ASSERT_TRUE(strncasecmp("abcd", "abc", 4) > 0); + TEST_ASSERT_EQ(strncasecmp("hello", "HELLO world", 5), 0); + return 0; +} + +static int +test_ffs(void) +{ + TEST_ASSERT_EQ(ffs(0), 0); + TEST_ASSERT_EQ(ffs(1), 1); + TEST_ASSERT_EQ(ffs(2), 2); + TEST_ASSERT_EQ(ffs(8), 4); + TEST_ASSERT_EQ(ffs(0x8000), 16); + TEST_ASSERT_EQ(ffs(-1), 1); /* all bits set: lowest bit is bit 0 */ + TEST_ASSERT_EQ(ffs(INT_MIN), 32); + + TEST_ASSERT_EQ(ffsl(0), 0); + TEST_ASSERT_EQ(ffsl(1L << 40), 41); + TEST_ASSERT_EQ(ffsl(LONG_MIN), 64); /* long is 64-bit on x86_64 */ + + TEST_ASSERT_EQ(ffsll(0), 0); + TEST_ASSERT_EQ(ffsll(0x100000000LL), 33); + TEST_ASSERT_EQ(ffsll(LLONG_MIN), 64); + return 0; +} + +static int +test_bcopy(void) +{ + char buf[16] = "abcdef"; + char buf2[16] = "abcdef"; + + /* Non-overlapping copy; NOTE the (src, dst) argument order. */ + bcopy(buf, buf2, 7); + TEST_ASSERT_STREQ(buf2, "abcdef"); + + /* Overlap, dst == src + 1: only a backward (memmove-style) copy can + * produce this; a forward copy smears src[0] over the whole range. */ + bcopy(buf, buf + 1, 5); + TEST_ASSERT_STREQ(buf, "aabcde"); + + /* Overlap, dst inside src at +2: both directions agree, sanity only. */ + { + char buf3[16] = "abcdef"; + + bcopy(buf3, buf3 + 2, 4); + TEST_ASSERT_STREQ(buf3, "ababcd"); + } + + /* Overlap, dst before src (backward region): forward copy is correct. */ + { + char buf4[16] = "abcdef"; + + bcopy(buf4 + 2, buf4, 4); + TEST_ASSERT_STREQ(buf4, "cdefef"); + } + return 0; +} + +static int +test_bzero(void) +{ + char buf[8]; + int i; + + for (i = 0; i < 8; i++) + { + buf[i] = 'A'; + } + bzero(buf, 4); + TEST_ASSERT_EQ(buf[0], '\0'); + TEST_ASSERT_EQ(buf[3], '\0'); + TEST_ASSERT_EQ(buf[4], 'A'); /* untouched past n */ + + bzero(buf, 0); /* n == 0: nothing */ + TEST_ASSERT_EQ(buf[0], '\0'); + TEST_ASSERT_EQ(buf[4], 'A'); + return 0; +} + +static int +test_bcmp(void) +{ + static const unsigned char hi[] = {0xff, 0x00}; + static const unsigned char lo[] = {0x7f, 0x00}; + + TEST_ASSERT_EQ(bcmp("abc", "abc", 3), 0); + TEST_ASSERT_TRUE(bcmp("abc", "abd", 3) != 0); + TEST_ASSERT_EQ(bcmp("abc", "abd", 2), 0); + TEST_ASSERT_EQ(bcmp("", "", 0), 0); + /* Bytes compare as unsigned char; a NUL does not end the comparison. */ + TEST_ASSERT_TRUE(bcmp(hi, lo, 1) != 0); + TEST_ASSERT_EQ(bcmp(hi, hi, 2), 0); + TEST_ASSERT_EQ(bcmp("a\0b", "a\0c", 3) != 0, 1); + return 0; +} + +static int +test_index(void) +{ + char s[] = "hello"; + + TEST_ASSERT_EQ(index(s, 'l') - s, 2); + TEST_ASSERT_EQ(index(s, 'h') - s, 0); + TEST_ASSERT_EQ(index(s, '\0') - s, 5); /* the NUL counts */ + TEST_ASSERT_NULL(index(s, 'z')); + /* c converts through unsigned char: 0x100 is byte 0, the NUL. */ + TEST_ASSERT_EQ(index(s, 0x100) - s, 5); + return 0; +} + +static int +test_rindex(void) +{ + char s[] = "hello"; + + TEST_ASSERT_EQ(rindex(s, 'l') - s, 3); + TEST_ASSERT_EQ(rindex(s, 'h') - s, 0); + TEST_ASSERT_EQ(rindex(s, '\0') - s, 5); /* the NUL counts */ + TEST_ASSERT_NULL(rindex(s, 'z')); + return 0; +} + +/* ---- failure mode: the plan's absent-character scenario ---- */ + +static int +test_absent_char(void) +{ + TEST_ASSERT_NULL(index("abc", 'z')); + TEST_ASSERT_NULL(rindex("abc", 'z')); + return 0; +} + +/* ---- register + runner ---- */ + +static const struct vlibc_test tests[] = { + {"strcasecmp", test_strcasecmp}, + {"strncasecmp", test_strncasecmp}, + {"ffs", test_ffs}, + {"bcopy", test_bcopy}, + {"bzero", test_bzero}, + {"bcmp", test_bcmp}, + {"index", test_index}, + {"rindex", test_rindex}, +}; + +/* + * Own main (not TEST_MAIN): supports the -f failure 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 && argv[1][0] == '-' && argv[1][1] == 'f' && argv[1][2] == '\0') + { + int before = vlibc_test_failures; + + vlibc_test_say(1, "RUN absent-char: "); + if (test_absent_char() == 0 && vlibc_test_failures == before) + { + vlibc_test_say(1, "PASS\n"); + vlibc_test_say(1, "SUMMARY: 1/1 passed, 0 assertion failure(s)\n"); + return 0; + } + vlibc_test_say(1, "FAIL\n"); + return 1; + } + + 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; +} + +#else /* !VLIBC_LEVEL_GE(2) */ + +/* + * Level 1: every function is gated at level 2, so there is + * nothing to run. Keep the TU compilable at any configured profile. + */ +int +main(void) +{ + vlibc_test_say(1, "SKIP: is level 2, not available here\n"); + return 0; +} + +#endif /* VLIBC_LEVEL_GE(2) */