From 2ad9928c52a4c7842bf49a943293367fc2fc4c2e Mon Sep 17 00:00:00 2001 From: frapank Date: Sun, 5 Jul 2026 13:22:57 +0200 Subject: [PATCH 1/6] add: dstrinsert --- dstr.c | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dstr.h | 13 +++++++++++++ 2 files changed, 70 insertions(+) diff --git a/dstr.c b/dstr.c index 5864277..7a99d48 100644 --- a/dstr.c +++ b/dstr.c @@ -561,6 +561,63 @@ dstr dstrappend_custom(dstr s1, const dstr s2, size_t cap) return _dstr_concat_impl(s1, dstrlen(s1), s2, dstrlen(s2), cap); } +// dstrinsert +dstr dstrinsert(dstr s1, ssize_t idx, const char* s2) +{ + if (!s1 || !s2) + return s1; + + size_t s1_len = dstrlen(s1); + size_t s2_len = strlen(s2); + if (s2_len == 0) + return s1; + if (s2_len > SIZE_MAX - s1_len) + return NULL; + + if (idx < 0) { + idx += (ssize_t)s1_len; + if (idx < 0) + idx = 0; + } + size_t pos = ((size_t)idx > s1_len) ? s1_len : (size_t)idx; + size_t new_len = s1_len + s2_len; + + uintptr_t s1_addr = (uintptr_t)s1; + uintptr_t s2_addr = (uintptr_t)s2; + size_t s1_cap = dstrcap(s1); + bool s2_aliases_s1 = s2_addr >= s1_addr && s2_addr - s1_addr < s1_cap; + size_t s2_offset = s2_aliases_s1 ? (size_t)(s2_addr - s1_addr) : 0; + + dstr tmp = dstrreserve(s1, _dstr_grow_cap(s1_cap, new_len + 1)); + if (!tmp) + return NULL; + s1 = tmp; + + if (s2_aliases_s1) + s2 = s1 + s2_offset; + + enum dstrhd_type t; + void* hd = _dstr_get_hdr_and_type(s1, &t); + + size_t tail_len = s1_len - pos + 1; + + if (s2_aliases_s1 && s2_offset + s2_len > pos) { + char* scratch = DSTR_MALLOC(s2_len); + if (!scratch) + return NULL; + memcpy(scratch, s2, s2_len); + memmove(s1 + pos + s2_len, s1 + pos, tail_len); + memcpy(s1 + pos, scratch, s2_len); + DSTR_FREE(scratch); + } else { + memmove(s1 + pos + s2_len, s1 + pos, tail_len); + memcpy(s1 + pos, s2, s2_len); + } + + _dstr_set_len(hd, new_len, t); + return s1; +} + // strnew static dstr _dstrnew_allocator(enum dstrhd_type t, const char* msg, diff --git a/dstr.h b/dstr.h index 10792b2..fab1db3 100644 --- a/dstr.h +++ b/dstr.h @@ -282,6 +282,19 @@ dstr dstrcat_custom(dstr s1, const char* s2, size_t cap) W_UNUSED_RESULT; dstr dstrappend_base(dstr s1, const dstr s2) W_UNUSED_RESULT; dstr dstrappend_custom(dstr s1, const dstr s2, size_t cap) W_UNUSED_RESULT; +/* + * dstrinsert(s1, idx, s2) + * + * Insert the null-terminated string s2 into s1 at idx, in place. Negative + * indices count from the end of the string, as in s[len + idx]. Out-of-range + * indices are clamped instead of erroring, so an idx at or before the start + * prepends and an idx at or past the end appends. + * May reallocate s1; always use the returned pointer. + * Returns s1 unchanged if s1 or s2 is NULL, or if s2 is empty. + * Returns NULL on allocation failure. + */ +dstr dstrinsert(dstr s1, ssize_t idx, const char* s2) W_UNUSED_RESULT; + /* * dstrauto * From 46600c57add50a2ad787db113075a5bc9d0f48af Mon Sep 17 00:00:00 2001 From: frapank Date: Sun, 5 Jul 2026 13:23:02 +0200 Subject: [PATCH 2/6] test: dstrinsert --- tester.c | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/tester.c b/tester.c index 43e59a3..06a1b84 100644 --- a/tester.c +++ b/tester.c @@ -443,6 +443,100 @@ static void test_growth_correctness_across_reallocs(void) { dstrfree(s); } +static void test_insert_basic(void) { + dstr s = dstrnew("Hello World"); + s = dstrinsert(s, 5, ","); + ASSERT_STR_EQUAL("Hello, World", s); + ASSERT_TRUE(dstrlen(s) == 12); + dstrfree(s); +} + +static void test_insert_prepend_and_append(void) { + dstr s = dstrnew("World"); + s = dstrinsert(s, 0, "Hello "); + ASSERT_STR_EQUAL("Hello World", s); + dstrfree(s); + + s = dstrnew("Hello"); + s = dstrinsert(s, 5, " World"); + ASSERT_STR_EQUAL("Hello World", s); + dstrfree(s); + + s = dstrnew("Hello"); + s = dstrinsert(s, 1000, "!"); + ASSERT_STR_EQUAL("Hello!", s); + dstrfree(s); +} + +static void test_insert_negative_index(void) { + dstr s = dstrnew("Hello World"); + s = dstrinsert(s, -5, "-"); + ASSERT_STR_EQUAL("Hello -World", s); + dstrfree(s); + + s = dstrnew("abc"); + s = dstrinsert(s, -1000, "X"); + ASSERT_STR_EQUAL("Xabc", s); + dstrfree(s); +} + +static void test_insert_edge_cases(void) { + dstr s = dstrnew("abc"); + + ASSERT_TRUE(dstrinsert(NULL, 0, "x") == NULL); + + dstr same = dstrinsert(s, 1, NULL); + ASSERT_TRUE(same == s); + ASSERT_STR_EQUAL("abc", s); + + same = dstrinsert(s, 1, ""); + ASSERT_TRUE(same == s); + ASSERT_STR_EQUAL("abc", s); + + dstrfree(s); +} + +static void test_insert_self_alias(void) { + // Inserting a string into itself must not read moved/corrupted memory. + dstr s = dstrnew("abc"); + s = dstrinsert(s, 0, s); + ASSERT_STR_EQUAL("abcabc", s); + dstrfree(s); + + s = dstrnew("abc"); + s = dstrinsert(s, 1, s); + ASSERT_STR_EQUAL("aabcbc", s); + dstrfree(s); + + s = dstrnew("abc"); + s = dstrinsert(s, 3, s); + ASSERT_STR_EQUAL("abcabc", s); + dstrfree(s); + + s = dstrnew("abcdef"); + s = dstrinsert(s, 2, s + 3); + ASSERT_STR_EQUAL("abdefcdef", s); + dstrfree(s); +} + +static void test_insert_growth_across_reallocs(void) { + dstr s = dstrnew(""); + for (int i = 0; i < 200; i++) + s = dstrinsert(s, 0, "ab"); + + ASSERT_TRUE(dstrlen(s) == 400); + bool ok = true; + for (size_t i = 0; i < dstrlen(s); i += 2) { + if (s[i] != 'a' || s[i + 1] != 'b') { + ok = false; + break; + } + } + ASSERT_TRUE(ok); + + dstrfree(s); +} + static void test_auto_cleanup(void) { // Isolated block to test cleanup attribute { @@ -486,6 +580,12 @@ int main(void) { RUN_TEST(test_split_independent_allocations); RUN_TEST(test_growth_no_unnecessary_realloc); RUN_TEST(test_growth_correctness_across_reallocs); + RUN_TEST(test_insert_basic); + RUN_TEST(test_insert_prepend_and_append); + RUN_TEST(test_insert_negative_index); + RUN_TEST(test_insert_edge_cases); + RUN_TEST(test_insert_self_alias); + RUN_TEST(test_insert_growth_across_reallocs); RUN_TEST(test_auto_cleanup); printf("\n\033[1;34m=== FINAL REPORT ===\033[0m\n"); From 01f7643a2b6c3923ff8e3be8a5a142e24e867d76 Mon Sep 17 00:00:00 2001 From: frapank Date: Sun, 5 Jul 2026 13:23:06 +0200 Subject: [PATCH 3/6] docs: dstrinsert --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a6a01c9..cafdacf 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,16 @@ dstrrange(s, 2, -1); // keep from index 2 to the last character Keeps only the `[start, end]` slice of `s`, in place (`end` is inclusive). Negative indices count from the end of the string, i.e. `-1` is the last character. Out-of-range indices are clamped instead of erroring, and a `start` past `end` or past the end of the string yields an empty `dstr`. No allocation happens: the existing buffer is shifted with `memmove` and truncated in place. No-op if `s` is `NULL` or empty. +### Insert + +```c +s = dstrinsert(s, 5, ", "); // insert at index 5 +s = dstrinsert(s, 0, ">> "); // prepend +s = dstrinsert(s, -1, "!"); // insert before the last character +``` + +Inserts the null-terminated string `s2` into `s1` at `idx`, in place. Negative indices count from the end of the string, as in `dstrrange`. Out-of-range indices are clamped instead of erroring. May reallocate `s1`; always use the returned pointer. + ### Split ```c @@ -232,7 +242,6 @@ dstr s = $("hello", 64); ## Roadmap -- **dstrinsert** — insert substring at a given index - **dstrtrim** — strip leading/trailing whitespace or a given charset ## License From 0530e02ab1757a767fd38a7cf466d6b10f4ae012 Mon Sep 17 00:00:00 2001 From: frapank Date: Sun, 5 Jul 2026 13:23:51 +0200 Subject: [PATCH 4/6] add: dstrtrim --- dstr.c | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ dstr.h | 18 ++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/dstr.c b/dstr.c index 7a99d48..5731a5f 100644 --- a/dstr.c +++ b/dstr.c @@ -225,6 +225,80 @@ void dstrrange(dstr s, ssize_t start, ssize_t end) _dstr_set_len(hd, newlen, t); } +static inline bool _dstr_is_space(unsigned char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || + c == '\r'; +} + +// dstrtrim +void dstrtrim_base(dstr s) +{ + enum dstrhd_type t; + void* hd = _dstr_get_hdr_and_type(s, &t); + if (!hd) + return; + + size_t len = dstrlen(s); + if (len == 0) + return; + + size_t start = 0; + while (start < len && _dstr_is_space((unsigned char)s[start])) + start++; + + size_t end = len; + while (end > start && _dstr_is_space((unsigned char)s[end - 1])) + end--; + + size_t newlen = end - start; + if (start) + memmove(s, s + start, newlen); + s[newlen] = '\0'; + _dstr_set_len(hd, newlen, t); +} + +static inline void _dstr_cutset_bitmap(const char* cutset, uint64_t bitmap[4]) +{ + bitmap[0] = bitmap[1] = bitmap[2] = bitmap[3] = 0; + for (const unsigned char* p = (const unsigned char*)cutset; *p; p++) + bitmap[*p >> 6] |= (uint64_t)1 << (*p & 63); +} + +static inline bool _dstr_in_bitmap(const uint64_t bitmap[4], unsigned char c) +{ + return (bitmap[c >> 6] >> (c & 63)) & 1; +} + +void dstrtrim_custom(dstr s, const char* cutset) +{ + enum dstrhd_type t; + void* hd = _dstr_get_hdr_and_type(s, &t); + if (!hd || !cutset || cutset[0] == '\0') + return; + + size_t len = dstrlen(s); + if (len == 0) + return; + + uint64_t bitmap[4]; + _dstr_cutset_bitmap(cutset, bitmap); + + size_t start = 0; + while (start < len && _dstr_in_bitmap(bitmap, (unsigned char)s[start])) + start++; + + size_t end = len; + while (end > start && _dstr_in_bitmap(bitmap, (unsigned char)s[end - 1])) + end--; + + size_t newlen = end - start; + if (start) + memmove(s, s + start, newlen); + s[newlen] = '\0'; + _dstr_set_len(hd, newlen, t); +} + // dstrsplit dstr* dstrsplit(dstr s, const char* delim, size_t* out_count) { diff --git a/dstr.h b/dstr.h index fab1db3..80e98a0 100644 --- a/dstr.h +++ b/dstr.h @@ -47,6 +47,10 @@ typedef ptrdiff_t ssize_t; #define dstrappend(...) \ GET_DSTRAPPEND(__VA_ARGS__, dstrappend_custom, dstrappend_base)(__VA_ARGS__) +#define GET_DSTRTRIM(_1, _2, NAME, ...) NAME +#define dstrtrim(...) \ + GET_DSTRTRIM(__VA_ARGS__, dstrtrim_custom, dstrtrim_base)(__VA_ARGS__) + #define W_UNUSED_RESULT __attribute__((warn_unused_result)) typedef char* dstr; @@ -138,6 +142,20 @@ ssize_t dstrfind(dstr s, const char* needle) W_UNUSED_RESULT; */ void dstrrange(dstr s, ssize_t start, ssize_t end); +/* + * dstrtrim(s) + * dstrtrim(s, cutset) + * + * Strip leading and trailing bytes from s, in place. The one-argument form + * strips ASCII whitespace (' ', '\t', '\n', '\v', '\f', '\r'); the + * two-argument form strips any byte found in the null-terminated cutset + * instead. No allocation, no new pointer: the kept slice is shifted with + * memmove and truncated in place. No-op if s is NULL, empty, or (for the + * two-argument form) if cutset is NULL or empty. + */ +void dstrtrim_base(dstr s); +void dstrtrim_custom(dstr s, const char* cutset); + /* * dstrsplit(s, delim, out_count) * From 95a0ea47d1e0fc231b7e8b684248e3dba808cfb4 Mon Sep 17 00:00:00 2001 From: frapank Date: Sun, 5 Jul 2026 13:24:26 +0200 Subject: [PATCH 5/6] test: dstrtrim --- tester.c | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tester.c b/tester.c index 06a1b84..fa519e0 100644 --- a/tester.c +++ b/tester.c @@ -443,6 +443,72 @@ static void test_growth_correctness_across_reallocs(void) { dstrfree(s); } +static void test_trim_whitespace_basic(void) { + dstr s = dstrnew(" Hello World "); + dstrtrim(s); + ASSERT_STR_EQUAL("Hello World", s); + ASSERT_TRUE(dstrlen(s) == 11); + dstrfree(s); + + s = dstrnew("\t\n data \r\v\f"); + dstrtrim(s); + ASSERT_STR_EQUAL("data", s); + dstrfree(s); +} + +static void test_trim_no_change(void) { + dstr s = dstrnew("nothing to trim"); + size_t cap = dstrcap(s); + dstrtrim(s); + ASSERT_STR_EQUAL("nothing to trim", s); + ASSERT_TRUE(dstrcap(s) == cap); + dstrfree(s); +} + +static void test_trim_all_whitespace(void) { + dstr s = dstrnew(" \t\n "); + dstrtrim(s); + ASSERT_STR_EQUAL("", s); + ASSERT_TRUE(dstrlen(s) == 0); + dstrfree(s); +} + +static void test_trim_empty_and_null(void) { + dstr s = dstrnew(""); + dstrtrim(s); + ASSERT_STR_EQUAL("", s); + dstrfree(s); + + dstrtrim(NULL); + ASSERT_TRUE(true); +} + +static void test_trim_custom_cutset(void) { + dstr s = dstrnew("xxxHelloxxx"); + dstrtrim(s, "x"); + ASSERT_STR_EQUAL("Hello", s); + dstrfree(s); + + s = dstrnew("-+-value-+-"); + dstrtrim(s, "+-"); + ASSERT_STR_EQUAL("value", s); + dstrfree(s); +} + +static void test_trim_custom_cutset_edge_cases(void) { + dstr s = dstrnew("abc"); + dstrtrim(s, ""); + ASSERT_STR_EQUAL("abc", s); + + dstrtrim(s, NULL); + ASSERT_STR_EQUAL("abc", s); + + dstrtrim(s, "abc"); + ASSERT_STR_EQUAL("", s); + + dstrfree(s); +} + static void test_insert_basic(void) { dstr s = dstrnew("Hello World"); s = dstrinsert(s, 5, ","); @@ -580,6 +646,12 @@ int main(void) { RUN_TEST(test_split_independent_allocations); RUN_TEST(test_growth_no_unnecessary_realloc); RUN_TEST(test_growth_correctness_across_reallocs); + RUN_TEST(test_trim_whitespace_basic); + RUN_TEST(test_trim_no_change); + RUN_TEST(test_trim_all_whitespace); + RUN_TEST(test_trim_empty_and_null); + RUN_TEST(test_trim_custom_cutset); + RUN_TEST(test_trim_custom_cutset_edge_cases); RUN_TEST(test_insert_basic); RUN_TEST(test_insert_prepend_and_append); RUN_TEST(test_insert_negative_index); From c09caf12f57bbb38766455add9cd14b1d4abba01 Mon Sep 17 00:00:00 2001 From: frapank Date: Sun, 5 Jul 2026 13:24:57 +0200 Subject: [PATCH 6/6] docs: dstrtrim --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cafdacf..7e1008f 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,15 @@ dstrrange(s, 2, -1); // keep from index 2 to the last character Keeps only the `[start, end]` slice of `s`, in place (`end` is inclusive). Negative indices count from the end of the string, i.e. `-1` is the last character. Out-of-range indices are clamped instead of erroring, and a `start` past `end` or past the end of the string yields an empty `dstr`. No allocation happens: the existing buffer is shifted with `memmove` and truncated in place. No-op if `s` is `NULL` or empty. +### Trim + +```c +dstrtrim(s); // strip leading/trailing ASCII whitespace +dstrtrim(s, "xy"); // strip leading/trailing 'x'/'y' bytes instead +``` + +Strips leading and trailing bytes from `s`, in place. The one-argument form strips ASCII whitespace (`' '`, `'\t'`, `'\n'`, `'\v'`, `'\f'`, `'\r'`); the two-argument form strips any byte found in the null-terminated `cutset` instead. No allocation happens: the kept slice is shifted with `memmove` and truncated in place. No-op if `s` is `NULL` or empty, or (two-argument form) if `cutset` is `NULL` or empty. + ### Insert ```c @@ -240,10 +249,6 @@ dstr s = $("hello", 64); - All functions that return `dstr` may return a reallocated pointer; always reassign. - Capacity arguments are hints and are silently increased if insufficient. -## Roadmap - -- **dstrtrim** — strip leading/trailing whitespace or a given charset - ## License BSD-2-Clause