Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ cc -std=c11 your_file.c libdstr.a -o your_program

If not defined, these fall back to the standard allocators.

**Growth strategy** - controls how `dstrcat`, `dstrappend`, and `dstrpush` (the two-argument, non-custom forms) grow the buffer when the current capacity is not enough:

```c
// dstr_options.h
#define DSTR_GROWTH_ENABLED 1 /* 1 = amortized growth, 0 = allocate exactly what is needed */
#define DSTR_GROWTH_PERCENT 50 /* extra capacity added on growth, as a percent of the current capacity */
```

With `DSTR_GROWTH_ENABLED` set to 1 (the default), a growth only happens when the requested size exceeds the current capacity, and it adds `DSTR_GROWTH_PERCENT` percent of headroom on top (50 means the new capacity is roughly 1.5x the old one), so repeated small appends do not reallocate on every call. Setting it to 0 disables the headroom: every growth allocates exactly what is needed, trading more frequent reallocations for a tighter memory footprint. Either way, capacity is left untouched whenever it is already sufficient. The `_custom` variants of `dstrcat`, `dstrappend`, and `dstrpush`, as well as the `cap` argument of `dstrnew`, always honor the caller-supplied capacity and are not affected by these options.

## Types

`dstr` is a `char*`. The internal headers (`dstrhd8`, `dstrhd16`, `dstrhd32`, `dstrhd64`) are implementation details and not part of the public API. The header type used for a given string is selected automatically based on the required capacity.
Expand Down Expand Up @@ -157,6 +167,28 @@ ssize_t index = dstrfind(s, "needle");

Searches for the first occurrence of a null-terminated C string (`needle`) inside the dynamic string `s`. Returns the zero-based index of the first match, or `-1` if the substring is not found. It safely returns `-1` if either argument is `NULL`, if the needle is an empty string, or if the needle is longer than the string itself.

### Range (substring)

```c
dstr sub = dstrrange(s, 0, 3); // first 3 characters
dstr sub = dstrrange(s, -3, -1); // second-to-last and third-to-last characters
dstr sub = dstrrange(s, 2, -1); // from index 2 up to (not including) the last character
```

Returns a new `dstr` holding the `[start, end)` slice of `s` (`end` is exclusive). 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` at or past `end` yields an empty `dstr`. Returns `NULL` only if `s` is `NULL` or allocation fails. Free the result with `dstrfree`.

### Split

```c
size_t count;
dstr* parts = dstrsplit(s, ",", &count);
for (size_t i = 0; i < count; i++)
puts(parts[i]);
dstrsplitfree(parts, count);
```

Splits `s` on every occurrence of `delim`, returning a newly allocated array of `dstr`. Adjacent or leading/trailing delimiters produce empty (`""`) elements. Returns `NULL` if `s`/`delim` is `NULL`, `delim` is empty, or allocation fails. The array and every element must be released together with `dstrsplitfree`.

### Free

```c
Expand Down Expand Up @@ -197,10 +229,7 @@ dstr s = $("hello", 64);
## Roadmap

- **dstrinsert** — insert substring at a given index
- **dstrfind** — find first or last occurrence of a char or substring
- **dstrtrim** — strip leading/trailing whitespace or a given charset
- **dstrrange** — return a substring by start/end index
- **dstrsplit** — split by delimiter into an array of `dstr`

## License

Expand Down
161 changes: 147 additions & 14 deletions dstr.c
Original file line number Diff line number Diff line change
Expand Up @@ -154,40 +154,173 @@ static inline void* _dstr_get_hdr_and_type(dstr s, enum dstrhd_type* t_out)

static inline size_t _dstr_grow_cap(size_t old_cap, size_t needed)
{
if (needed <= old_cap)
return old_cap;

#if DSTR_GROWTH_ENABLED
size_t grown = old_cap < 16 ? 16 : old_cap;
if (grown <= SIZE_MAX - grown / 2)
grown += grown / 2;

if (grown <= SIZE_MAX / DSTR_GROWTH_PERCENT) {
size_t increment = grown * DSTR_GROWTH_PERCENT / 100;
if (grown <= SIZE_MAX - increment)
grown += increment;
}

return grown > needed ? grown : needed;
#else
return needed;
#endif
}

// strfind
ssize_t dstrfind(dstr s, const char* needle)
static inline ssize_t _dstr_find_from(const char* buf,
size_t len,
const char* needle,
size_t nlen,
size_t from)
{
if (!s || !needle || needle[0] == '\0')
if (from > len || nlen > len - from)
return -1;

size_t len = dstrlen(s);
size_t nlen = strlen(needle);

if (nlen > len)
return -1;

size_t remaining = len - nlen + 1;
const char* cursor = s;
size_t remaining = len - from - nlen + 1;
const char* cursor = buf + from;

while (remaining > 0) {
const char* hit = memchr(cursor, needle[0], remaining);
if (!hit)
return -1;
if (memcmp(hit, needle, nlen) == 0)
return hit - s;
return hit - buf;
remaining -= (size_t)(hit - cursor) + 1;
cursor = hit + 1;
}

return -1;
}

static dstr _dstr_slice_alloc(const char* buf, size_t len)
{
enum dstrhd_type t = _dstr_type_by_size(len + 1);
size_t hd_size = _dstr_size_by_type(t);

void* hd = DSTR_MALLOC(hd_size + len + 1);
if (!hd)
return NULL;

_dstr_set_len(hd, len, t);
_dstr_set_cap(hd, len + 1, t);
*((uint8_t*)hd + hd_size - 1) = (uint8_t)t;

char* buf_out = (char*)hd + hd_size;
memcpy(buf_out, buf, len);
buf_out[len] = '\0';

return buf_out;
}

// strfind
ssize_t dstrfind(dstr s, const char* needle)
{
if (!s || !needle || needle[0] == '\0')
return -1;

return _dstr_find_from(s, dstrlen(s), needle, strlen(needle), 0);
}

// dstrrange
dstr dstrrange(dstr s, ssize_t start, ssize_t end)
{
if (!s)
return NULL;

size_t len_u = dstrlen(s);
ssize_t len = (len_u > (size_t)PTRDIFF_MAX) ? PTRDIFF_MAX : (ssize_t)len_u;

if (start < 0)
start += len;
if (start < 0)
start = 0;
if (start > len)
start = len;

if (end < 0)
end += len;
if (end < 0)
end = 0;
if (end > len)
end = len;

size_t sub_len = (start >= end) ? 0 : (size_t)(end - start);

return _dstr_slice_alloc(s + start, sub_len);
}

// dstrsplit
dstr* dstrsplit(dstr s, const char* delim, size_t* out_count)
{
if (out_count)
*out_count = 0;

if (!s || !delim || delim[0] == '\0')
return NULL;

size_t len = dstrlen(s);
size_t dlen = strlen(delim);

size_t count = 1;
size_t pos = 0;
for (;;) {
ssize_t idx = _dstr_find_from(s, len, delim, dlen, pos);
if (idx < 0)
break;
count++;
pos = (size_t)idx + dlen;
}

if (count > SIZE_MAX / sizeof(dstr))
return NULL;

dstr* parts = DSTR_MALLOC(count * sizeof(dstr));
if (!parts)
return NULL;

size_t n = 0;
pos = 0;
for (;;) {
ssize_t idx = _dstr_find_from(s, len, delim, dlen, pos);
size_t piece_end = (idx < 0) ? len : (size_t)idx;

parts[n] = _dstr_slice_alloc(s + pos, piece_end - pos);
if (!parts[n]) {
for (size_t i = 0; i < n; i++)
dstrfree(parts[i]);
DSTR_FREE(parts);
return NULL;
}
n++;

if (idx < 0)
break;
pos = piece_end + dlen;
}

if (out_count)
*out_count = n;

return parts;
}

// dstrsplitfree
void dstrsplitfree(dstr* parts, size_t count)
{
if (!parts)
return;

for (size_t i = 0; i < count; i++)
dstrfree(parts[i]);

DSTR_FREE(parts);
}

// strlen
size_t dstrlen(dstr s)
{
Expand Down
32 changes: 32 additions & 0 deletions dstr.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,38 @@ void dstrfree(dstr s);
*/
ssize_t dstrfind(dstr s, const char* needle) W_UNUSED_RESULT;

/*
* dstrrange(s, start, end)
*
* Return a new dstr with a copy of the [start, end) slice of s (end is
* exclusive). Negative indices count from the end of the string, as in
* s[len + start]. Out-of-range indices are clamped to [0, len], and a
* start at or past end yields an empty (but non-NULL) dstr.
* Returns NULL if s is NULL or on allocation failure. The result is an
* independent allocation; free it with dstrfree.
*/
dstr dstrrange(dstr s, ssize_t start, ssize_t end) W_UNUSED_RESULT;

/*
* dstrsplit(s, delim, out_count)
*
* Split s on every occurrence of the null-terminated delim, returning a
* newly allocated array of dstr. Adjacent or leading/trailing delimiters
* produce empty ("") elements. If out_count is not NULL, the number of
* elements is written to it.
* Returns NULL (and *out_count = 0) if s or delim is NULL, delim is
* empty, or on allocation failure. Free the result with dstrsplitfree.
*/
dstr* dstrsplit(dstr s, const char* delim, size_t* out_count) W_UNUSED_RESULT;

/*
* dstrsplitfree(parts, count)
*
* Free an array returned by dstrsplit, including each element. Passing
* NULL is a no-op.
*/
void dstrsplitfree(dstr* parts, size_t count);

/*
* dstrlen(s) - number of characters in s, excluding the null terminator.
* dstrcap(s) - total allocated capacity in bytes.
Expand Down
16 changes: 16 additions & 0 deletions dstr_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,20 @@
#define DSTR_REALLOC(p, s) realloc(p, s)
#define DSTR_FREE(s) free(s)

/*
* Amortized growth for the dstrcat/dstrappend/dstrpush base variants.
* 1 = grow ahead of need to reduce future reallocations
* 0 = allocate exactly what is needed, nothing more
* The custom variants (and dstrnew's cap argument) always honor the
* caller-supplied capacity regardless of this setting.
*/
#define DSTR_GROWTH_ENABLED 1

/*
* Extra capacity requested on growth, as a percentage of the current
* capacity. Only used when DSTR_GROWTH_ENABLED is 1. Must be > 0.
* 50 means +50% (new capacity is roughly 1.5x the old one).
*/
#define DSTR_GROWTH_PERCENT 50

#endif
Loading
Loading