master ohmu/fastfetch-openbsd / src / common / impl / FFstrbuf.c
ohmu  ·  2026-10-15
  1#include "common/FFstrbuf.h"
  2#include "common/mallocHelper.h"
  3#include "common/strutil.h"
  4#include "common/debug.h"
  5
  6#include <ctype.h>
  7#include <inttypes.h>
  8#include <math.h>
  9
 10char* CHAR_NULL_PTR = "";
 11
 12void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate) {
 13    strbuf->allocated = allocate;
 14
 15    if (strbuf->allocated > 0) {
 16        strbuf->chars = (char*) malloc(sizeof(char) * strbuf->allocated);
 17    }
 18
 19    // This will set the length to zero and the null byte.
 20    ffStrbufClear(strbuf);
 21}
 22
 23void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
 24    char* buffer = nullptr;
 25    int len = vasprintf(&buffer, format, arguments);
 26    assert(len >= 0);
 27
 28    ffStrbufInitMoveNS(strbuf, (uint32_t) len, buffer);
 29}
 30
 31// Takes ownership of `heapStr`. The caller must not free `heapStr` after calling this
 32// function; the memory will be managed and freed via the associated FFstrbuf.
 33void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr) {
 34    strbuf->length = length;
 35    size_t allocSize = ffMallocUsableSize(heapStr);
 36    if (allocSize == 0) {
 37        allocSize = length + 1;
 38    } else if (allocSize > UINT32_MAX) {
 39        allocSize = UINT32_MAX;
 40    }
 41    strbuf->allocated = (uint32_t) allocSize;
 42    strbuf->chars = heapStr;
 43}
 44
 45void ffStrbufInitF(FFstrbuf* strbuf, const char* format, ...) {
 46    va_list arguments;
 47    va_start(arguments, format);
 48    ffStrbufInitVF(strbuf, format, arguments);
 49    va_end(arguments);
 50}
 51
 52FFstrbuf ffStrbufCreateF(const char* format, ...) {
 53    FFstrbuf strbuf;
 54
 55    va_list arguments;
 56    va_start(arguments, format);
 57    ffStrbufInitVF(&strbuf, format, arguments);
 58    va_end(arguments);
 59
 60    return strbuf;
 61}
 62
 63void ffStrbufEnsureFreeNoCheck(FFstrbuf* strbuf, uint32_t free) {
 64    uint32_t allocate;
 65    if (__builtin_expect(__builtin_uadd_overflow(strbuf->length, free, &allocate), false)) {
 66        FF_DEBUG("Error: Integer overflow when calculating allocation size. Aborting");
 67        abort();
 68    }
 69
 70    if (allocate < FASTFETCH_STRBUF_DEFAULT_ALLOC) { // `<` for null terminator
 71        allocate = FASTFETCH_STRBUF_DEFAULT_ALLOC;
 72    } else {
 73        if (__builtin_expect(allocate > (UINT32_MAX >> 1), false)) {
 74            // User tried to allocate more than 2GB of memory, which exceeds the maximum size supported by FFstrbuf.
 75            // This is likely an error or an attempt to exploit the program. Abort to prevent potential issues.
 76            FF_DEBUG("Error: Attempted to allocate %" PRIu32 " bytes more than 2GB of memory in FFstrbuf. Aborting", allocate);
 77            abort();
 78        }
 79
 80        // Round up to the next power of 2.
 81        // If the value is already a power of 2, it will be rounded up to the next power of 2.
 82        // allocate = __builtin_stdc_bit_ceil(allocate + 1); // Currently generates worse asm code
 83        allocate = 1U << (32 - __builtin_clz(allocate));
 84    }
 85
 86    if (strbuf->allocated == 0) {
 87        char* newbuf = malloc(sizeof(*strbuf->chars) * allocate);
 88        if (strbuf->length == 0) {
 89            *newbuf = '\0';
 90        } else {
 91            memcpy(newbuf, strbuf->chars, strbuf->length + 1);
 92        }
 93        strbuf->chars = newbuf;
 94    } else {
 95        strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * allocate);
 96    }
 97
 98    strbuf->allocated = allocate;
 99}
100
101// Ensure that at least `free` bytes are available in the buffer besides the current length
102// for an empty buffer, free + 1 length memory will be allocated(+1 for the NUL)
103// This function ensures a dynamic buffer is allocated even if free == 0
104void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free) {
105    uint32_t newCap;
106
107    if (strbuf->allocated == 0) {
108        assert(strbuf->length < UINT32_MAX - 1); // We don't use static strings with length >= UINT32_MAX - 1, so this should never happen
109        if (__builtin_expect(__builtin_uadd_overflow(strbuf->length + 1, free, &newCap), false)) {
110            FF_DEBUG("Error: Integer overflow when calculating new capacity. Aborting");
111            abort();
112        }
113
114        char* newbuf = malloc(sizeof(*strbuf->chars) * newCap);
115        if (strbuf->length == 0) {
116            *newbuf = '\0';
117        } else {
118            memcpy(newbuf, strbuf->chars, strbuf->length + 1);
119        }
120        strbuf->chars = newbuf;
121    } else {
122        uint32_t oldFree = ffStrbufGetFree(strbuf);
123        if (oldFree >= free) {
124            return;
125        }
126
127        if (__builtin_expect(__builtin_uadd_overflow(strbuf->allocated, free - oldFree, &newCap), false)) {
128            FF_DEBUG("Error: Integer overflow when calculating new capacity. Aborting");
129            abort();
130        }
131        strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * newCap);
132    }
133
134    strbuf->allocated = newCap;
135}
136
137void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int)) {
138    if (value == nullptr) {
139        return;
140    }
141
142    // Ensure capacity > 0 or the modification below will fail
143    uint32_t length = (uint32_t) strlen(value);
144    if (length == 0) {
145        return;
146    }
147
148    ffStrbufEnsureFree(strbuf, length);
149    for (uint32_t i = 0; value[i] != '\0'; i++) {
150        strbuf->chars[strbuf->length++] = (char) transformFunc(value[i]);
151    }
152    strbuf->chars[strbuf->length] = '\0';
153}
154
155void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
156    va_list copy;
157    va_copy(copy, arguments);
158
159    uint32_t free = ffStrbufGetFree(strbuf);
160    int written = vsnprintf(strbuf->chars + strbuf->length, strbuf->allocated > 0 ? free + 1 : 0, format, arguments);
161
162    if (written > 0 && (uint32_t) written > free) {
163        ffStrbufEnsureFreeNoCheck(strbuf, (uint32_t) written);
164        written = vsnprintf(strbuf->chars + strbuf->length, (uint32_t) written + 1, format, copy);
165    }
166
167    va_end(copy);
168
169    if (written > 0) {
170        strbuf->length += (uint32_t) written;
171    }
172}
173
174const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until) {
175    if (value == nullptr) {
176        return nullptr;
177    }
178
179    const char* end = strchr(value, until);
180    if (end == nullptr) {
181        ffStrbufAppendS(strbuf, value);
182    } else {
183        ffStrbufAppendNS(strbuf, (uint32_t) (end - value), value);
184    }
185    return end;
186}
187
188void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...) {
189    va_list arguments;
190    va_start(arguments, format);
191
192    if (strbuf->allocated == 0) {
193        ffStrbufInitVF(strbuf, format, arguments);
194        va_end(arguments);
195        return;
196    }
197
198    ffStrbufClear(strbuf);
199    ffStrbufAppendVF(strbuf, format, arguments);
200    va_end(arguments);
201}
202
203void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...) {
204    va_list arguments;
205    va_start(arguments, format);
206    ffStrbufAppendVF(strbuf, format, arguments);
207    va_end(arguments);
208}
209
210void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
211    if (value == nullptr || length == 0) {
212        return;
213    }
214
215    ffStrbufEnsureFree(strbuf, length);
216    memmove(strbuf->chars + length, strbuf->chars, strbuf->length + 1); // + 1 for the null byte
217    memcpy(strbuf->chars, value, length);
218    strbuf->length += length;
219}
220
221void ffStrbufPrependC(FFstrbuf* strbuf, char c) {
222    ffStrbufEnsureFree(strbuf, 1);
223    memmove(strbuf->chars + 1, strbuf->chars, strbuf->length + 1); // + 1 for the null byte
224    strbuf->chars[0] = c;
225    strbuf->length += 1;
226}
227
228void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
229    if (length == 0) {
230        ffStrbufClear(strbuf);
231        return;
232    }
233
234    if (strbuf->allocated <= length) {
235        char* newBuf = malloc(sizeof(char) * (length + 1));
236        memcpy(newBuf, value, length);
237        if (strbuf->allocated > 0) {
238            free(strbuf->chars);
239        }
240        strbuf->chars = newBuf;
241        strbuf->allocated = length + 1;
242    } else {
243        memmove(strbuf->chars, value, length);
244    }
245
246    strbuf->length = length;
247    strbuf->chars[length] = '\0';
248}
249
250void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value) {
251    // `value` is non-null per the `nonnull(2)` contract; the aliasing check cannot be expressed by an attribute
252    assert(value != strbuf);
253
254    if (value->length == 0) {
255        ffStrbufClear(strbuf);
256        return;
257    }
258
259    if (value->allocated == 0) // static string
260    {
261        if (strbuf->allocated != 0) {
262            free(strbuf->chars);
263            strbuf->allocated = 0;
264        }
265        strbuf->chars = value->chars;
266        strbuf->length = value->length;
267        return;
268    }
269    ffStrbufSetNS(strbuf, value->length, value->chars);
270}
271
272void ffStrbufTrimLeft(FFstrbuf* strbuf, char c) {
273    if (strbuf->length == 0) {
274        return;
275    }
276
277    uint32_t index = 0;
278    while (index < strbuf->length && strbuf->chars[index] == c) {
279        ++index;
280    }
281
282    if (index == 0) {
283        return;
284    }
285
286    if (strbuf->allocated == 0) {
287        // static string
288        strbuf->length -= index;
289        strbuf->chars += index;
290        return;
291    }
292
293    memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index);
294    strbuf->length -= index;
295    strbuf->chars[strbuf->length] = '\0';
296}
297
298void ffStrbufTrimRight(FFstrbuf* strbuf, char c) {
299    if (strbuf->length == 0) {
300        return;
301    }
302
303    if (!ffStrbufEndsWithC(strbuf, c)) {
304        return;
305    }
306
307    do {
308        --strbuf->length;
309    } while (ffStrbufEndsWithC(strbuf, c));
310
311    if (strbuf->allocated == 0) {
312        // static string
313        ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars);
314        return;
315    }
316
317    strbuf->chars[strbuf->length] = '\0';
318}
319
320void ffStrbufTrimLeftSpace(FFstrbuf* strbuf) {
321    if (strbuf->length == 0) {
322        return;
323    }
324
325    uint32_t index = 0;
326    while (index < strbuf->length && isspace(strbuf->chars[index])) {
327        ++index;
328    }
329
330    if (index == 0) {
331        return;
332    }
333
334    if (strbuf->allocated == 0) {
335        // static string
336        strbuf->length -= index;
337        strbuf->chars += index;
338        return;
339    }
340
341    memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index);
342    strbuf->length -= index;
343    strbuf->chars[strbuf->length] = '\0';
344}
345
346void ffStrbufTrimRightSpace(FFstrbuf* strbuf) {
347    if (strbuf->length == 0) {
348        return;
349    }
350
351    if (!ffStrbufEndsWithFn(strbuf, isspace)) {
352        return;
353    }
354
355    do {
356        --strbuf->length;
357    } while (ffStrbufEndsWithFn(strbuf, isspace));
358
359    if (strbuf->allocated == 0) {
360        // static string
361        ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars);
362        return;
363    }
364
365    strbuf->chars[strbuf->length] = '\0';
366}
367
368bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex) {
369    if (startIndex > strbuf->length || startIndex >= endIndex) {
370        return false;
371    }
372
373    if (endIndex > strbuf->length) {
374        ffStrbufSubstrBefore(strbuf, startIndex);
375        return true;
376    }
377
378    ffStrbufEnsureFree(strbuf, 0);
379    memmove(strbuf->chars + startIndex, strbuf->chars + endIndex, strbuf->length - endIndex);
380    strbuf->length -= (endIndex - startIndex);
381    strbuf->chars[strbuf->length] = '\0';
382    return true;
383}
384
385void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str) {
386    uint32_t stringLength = (uint32_t) strlen(str);
387
388    for (uint32_t i = ffStrbufNextIndexS(strbuf, 0, str); i < strbuf->length; i = ffStrbufNextIndexS(strbuf, i, str)) {
389        ffStrbufRemoveSubstr(strbuf, i, i + stringLength);
390    }
391}
392
393void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]) {
394    for (uint32_t i = 0; i < numStrings; i++) {
395        ffStrbufRemoveS(strbuf, strings[i]);
396    }
397}
398
399void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace) {
400    if (strbuf->length == 0) {
401        return;
402    }
403
404    ffStrbufEnsureFree(strbuf, 0);
405    for (
406        char* current_pos = memchr(strbuf->chars, find, strbuf->length);
407        current_pos;
408        current_pos = memchr(
409            current_pos + 1,
410            find,
411            strbuf->length - (uint32_t) (current_pos + 1 - strbuf->chars))) {
412        *current_pos = replace;
413    }
414}
415
416bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index) {
417    if (strbuf->length <= index) {
418        return false;
419    }
420
421    if (strbuf->allocated == 0) {
422        // static string
423        ffStrbufInitNS(strbuf, index, strbuf->chars);
424        return true;
425    }
426
427    strbuf->length = index;
428    strbuf->chars[strbuf->length] = '\0';
429    return true;
430}
431
432bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index) {
433    if (index >= strbuf->length) {
434        ffStrbufClear(strbuf);
435        return true;
436    }
437
438    if (strbuf->allocated == 0) {
439        // static string
440        strbuf->length -= index + 1;
441        strbuf->chars += index + 1;
442        return true;
443    }
444
445    memmove(strbuf->chars, strbuf->chars + index + 1, strbuf->length - index - 1);
446    strbuf->length -= (index + 1);
447    strbuf->chars[strbuf->length] = '\0';
448    return true;
449}
450
451bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c) {
452    uint32_t index = ffStrbufFirstIndexC(strbuf, c);
453    if (index >= strbuf->length) {
454        return false;
455    }
456    ffStrbufSubstrAfter(strbuf, index);
457    return true;
458}
459
460bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str) {
461    if (*str == '\0') {
462        return false;
463    }
464
465    uint32_t index = ffStrbufFirstIndexS(strbuf, str) + (uint32_t) strlen(str) - 1; // -1, because firstIndexS is already pointing to str[0], we want to add only the remaining length
466    if (index >= strbuf->length) {
467        return false;
468    }
469
470    ffStrbufSubstrAfter(strbuf, index);
471    return true;
472}
473
474bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c) {
475    uint32_t index = ffStrbufLastIndexC(strbuf, c);
476    if (index >= strbuf->length) {
477        return false;
478    }
479
480    ffStrbufSubstrAfter(strbuf, index);
481    return true;
482}
483
484bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end) {
485    if (__builtin_expect(start >= end, false)) {
486        ffStrbufClear(strbuf);
487        return false;
488    }
489
490    if (__builtin_expect(start == 0, false)) {
491        return ffStrbufSubstrBefore(strbuf, end);
492    }
493    if (__builtin_expect(end >= strbuf->length, false)) {
494        return ffStrbufSubstrAfter(strbuf, start - 1);
495    }
496
497    uint32_t len = end - start;
498    ffStrbufEnsureFixedLengthFree(strbuf, len); // In case of static string
499    memmove(strbuf->chars, strbuf->chars + start, len);
500
501    strbuf->length = len;
502    strbuf->chars[len] = '\0';
503    return true;
504}
505
506uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c) {
507    uint32_t result = 0;
508    for (uint32_t i = 0; i < strbuf->length; i++) {
509        if (strbuf->chars[i] == c) {
510            result++;
511        }
512    }
513
514    return result;
515}
516
517bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end) {
518    uint32_t endLength = (uint32_t) strlen(end);
519    if (ffStrbufEndsWithIgnCaseNS(strbuf, endLength, end)) {
520        ffStrbufSubstrBefore(strbuf, strbuf->length - endLength);
521        return true;
522    }
523
524    return false;
525}
526
527bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c) {
528    if (ffStrbufEndsWithC(strbuf, c)) {
529        return false;
530    }
531
532    ffStrbufAppendC(strbuf, c);
533    return true;
534}
535
536void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value) {
537    ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number
538    char* start = strbuf->chars + strbuf->length;
539
540    yyjson_val val = {};
541    unsafe_yyjson_set_sint(&val, value);
542    char* end = yyjson_write_number(&val, start);
543
544    assert(end != nullptr);
545
546    strbuf->length += (uint32_t) (end - start);
547}
548
549void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value) {
550    ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number
551    char* start = strbuf->chars + strbuf->length;
552
553    yyjson_val val = {};
554    unsafe_yyjson_set_uint(&val, value);
555    char* end = yyjson_write_number(&val, start);
556
557    assert(end != nullptr);
558
559    strbuf->length += (uint32_t) (end - start);
560}
561
562void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros) {
563    assert(precision <= 15); // yyjson_write_number supports up to 15 digits after the decimal point
564
565    ffStrbufEnsureFree(strbuf, 40); // Required by yyjson_write_number
566    char* start = strbuf->chars + strbuf->length;
567
568    if (precision == 0) {
569        value = round(value);
570    }
571    yyjson_val val = {};
572    unsafe_yyjson_set_double(&val, value);
573    if (precision > 0) {
574        unsafe_yyjson_set_fp_to_fixed(&val, precision);
575    }
576
577    // Write at most <precision> digits after the decimal point; doesn't append trailing zeros
578    char* end = yyjson_write_number(&val, start);
579
580    assert(end > start);
581
582    strbuf->length += (uint32_t) (end - start);
583
584    if (__builtin_expect(value > 1e21 || value < -1e21, false)) {
585        // If the value is too large, yyjson_write_number will write it in scientific notation
586        return;
587    }
588
589    if (trailingZeros) {
590        if (precision > 1) {
591            for (char* p = end - 1; *p != '.' && p > start; --p) {
592                --precision;
593            }
594            if (precision > 0) {
595                ffStrbufAppendNC(strbuf, (uint32_t) precision, '0');
596            }
597        } else if (precision == 0 || (precision < 0 && end[-1] == '0')) {
598            goto removeDecimalPoint;
599        }
600    } else {
601        if (end[-1] == '0') {
602        removeDecimalPoint:
603            // yyjson always appends ".0" to make it a float point number. We need to remove it
604            strbuf->length -= 2;
605            strbuf->chars[strbuf->length] = '\0';
606        }
607    }
608}
609
610void ffStrbufUpperCase(FFstrbuf* strbuf) {
611    for (uint32_t i = 0; i < strbuf->length; ++i) {
612        strbuf->chars[i] = (char) toupper(strbuf->chars[i]);
613    }
614}
615
616void ffStrbufLowerCase(FFstrbuf* strbuf) {
617    for (uint32_t i = 0; i < strbuf->length; ++i) {
618        strbuf->chars[i] = (char) tolower(strbuf->chars[i]);
619    }
620}
621
622void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c) {
623    if (num == 0) {
624        return;
625    }
626    if (index >= strbuf->length) {
627        index = strbuf->length;
628    }
629
630    ffStrbufEnsureFree(strbuf, num);
631    memmove(strbuf->chars + index + num, strbuf->chars + index, strbuf->length - index + 1);
632    memset(&strbuf->chars[index], c, num);
633    strbuf->length += num;
634}
635
636bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
637    assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
638    assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
639
640    const char* pBufferEnd = buffer->chars + buffer->length;
641    if (!*lineptr) {
642        *lineptr = buffer->chars;
643    } else {
644        *lineptr += *n;
645        if (*lineptr >= pBufferEnd) { // non-empty last line
646            return false;
647        }
648        **lineptr = delimiter;
649        ++*lineptr;
650    }
651    if (*lineptr >= pBufferEnd) { // empty last line
652        return false;
653    }
654
655    size_t remaining = (size_t) (pBufferEnd - *lineptr);
656    char* ending = memchr(*lineptr, delimiter, remaining);
657    if (ending) {
658        *n = (size_t) (ending - *lineptr);
659        *ending = '\0';
660    } else {
661        *n = remaining;
662    }
663    return true;
664}
665
666void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
667    assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
668    assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
669
670    if (!*lineptr) {
671        return;
672    }
673
674    *lineptr += *n;
675    if (*lineptr < buffer->chars + buffer->length) {
676        **lineptr = delimiter;
677    }
678}
679
680bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf) {
681    if (strbuf->allocated == 0) {
682        return false; // Doesn't work with static strings
683    }
684
685    bool changed = false;
686    for (uint32_t i = 0; i < strbuf->length; i++) {
687        if (strbuf->chars[i] != ' ') {
688            continue;
689        }
690
691        i++;
692        uint32_t j = i;
693        for (; j < strbuf->length && strbuf->chars[j] == ' '; j++);
694
695        if (j == i) {
696            continue;
697        }
698        memmove(&strbuf->chars[i], &strbuf->chars[j], strbuf->length - j + 1);
699        strbuf->length -= j - i;
700        changed = true;
701    }
702
703    return changed;
704}
705
706/// @brief Check if a separated string (comp) contains a substring (strbuf).
707/// @param strbuf The substring to check.
708/// @param compLength The length of the separated string to check.
709/// @param comp The separated string to check.
710/// @param separator The separator character.
711///
712/// Empty-string handling:
713/// - If `strbuf` is empty, it matches only if `comp` contains an empty segment
714///   (e.g. "abc::def", "abc:", ":abc") or `comp` itself is empty.
715/// - If `comp` is empty (compLength == 0), it matches only if `strbuf` is also empty.
716bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
717    if (__builtin_expect(compLength == 0, false)) {
718        return strbuf->length == 0;
719    }
720
721    for (const char* p = comp; p <= comp + compLength;) {
722        const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
723        if (colon == nullptr) {
724            uint32_t remainingLen = (uint32_t) (comp + compLength - p);
725            return strbuf->length == remainingLen && memcmp(strbuf->chars, p, remainingLen) == 0;
726        }
727
728        uint32_t substrLength = (uint32_t) (colon - p);
729        if (strbuf->length == substrLength && memcmp(strbuf->chars, p, substrLength) == 0) {
730            return true;
731        }
732
733        p = colon + 1;
734    }
735
736    return false;
737}
738
739/// @brief Case insensitive version of ffStrbufMatchSeparatedNS.
740bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
741    if (__builtin_expect(compLength == 0, false)) {
742        return strbuf->length == 0;
743    }
744
745    for (const char* p = comp; p <= comp + compLength;) {
746        const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
747        if (colon == nullptr) {
748            uint32_t remainingLen = (uint32_t) (comp + compLength - p);
749            return strbuf->length == remainingLen && strncasecmp(strbuf->chars, p, remainingLen) == 0;
750        }
751
752        uint32_t substrLength = (uint32_t) (colon - p);
753        if (strbuf->length == substrLength && strncasecmp(strbuf->chars, p, substrLength) == 0) {
754            return true;
755        }
756
757        p = colon + 1;
758    }
759
760    return false;
761}
762
763int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint) {
764    if (codepoint <= 0x7F) {
765        ffStrbufAppendC(strbuf, (char) codepoint);
766        return 1;
767    } else if (codepoint <= 0x7FF) {
768        ffStrbufAppendNS(strbuf, 2, (char[]) { (char) (0xC0 | (codepoint >> 6)), (char) (0x80 | (codepoint & 0x3F)) });
769        return 2;
770    } else if (codepoint <= 0xFFFF) {
771        ffStrbufAppendNS(strbuf, 3, (char[]) { (char) (0xE0 | (codepoint >> 12)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) });
772        return 3;
773    } else if (codepoint <= 0x10FFFF) {
774        ffStrbufAppendNS(strbuf, 4, (char[]) { (char) (0xF0 | (codepoint >> 18)), (char) (0x80 | ((codepoint >> 12) & 0x3F)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) });
775        return 4;
776    }
777
778    ffStrbufAppendS(strbuf, "�"); // U+FFFD REPLACEMENT CHARACTER
779    return 1;
780}
781
782/// @brief Check if a separated string (strbuf) contains a substring (comp).
783/// @param strbuf The separated to check.
784/// @param compLength The length of the separated string to check.
785/// @param comp The substring to check.
786/// @param separator The separator character.
787///
788/// Empty-string handling is symmetric to ffStrbufMatchSeparatedNS
789bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
790    if (__builtin_expect(strbuf->length == 0, false)) {
791        return compLength == 0; // An empty list contains only the empty segment
792    }
793
794    uint32_t startIndex = 0;
795    while (startIndex <= strbuf->length) { // `<=` so a trailing empty segment (e.g. "abc:") is checked
796        uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
797
798        uint32_t folderLength = colonIndex - startIndex;
799        if (folderLength == compLength && memcmp(strbuf->chars + startIndex, comp, compLength) == 0) {
800            return true;
801        }
802
803        startIndex = colonIndex + 1;
804    }
805
806    return false;
807}
808
809bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
810    if (__builtin_expect(strbuf->length == 0, false)) {
811        return compLength == 0; // An empty list contains only the empty segment
812    }
813
814    uint32_t startIndex = 0;
815    while (startIndex <= strbuf->length) { // `<=` so a trailing empty segment (e.g. "abc:") is checked
816        uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
817
818        uint32_t folderLength = colonIndex - startIndex;
819        if (folderLength == compLength && strncasecmp(strbuf->chars + startIndex, comp, compLength) == 0) {
820            return true;
821        }
822
823        startIndex = colonIndex + 1;
824    }
825
826    return false;
827}
828
829bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf) {
830    if (strbuf->length < 4) {
831        return false;
832    }
833
834    // Static string must be converted first.
835    assert(strbuf->allocated > 0);
836
837    bool changed = false;
838    uint32_t read = 0;
839    uint32_t write = 0;
840
841    while (read < strbuf->length) {
842        if (
843            read + 3 < strbuf->length &&
844            strbuf->chars[read] == '\\' &&
845            strbuf->chars[read + 1] == 'x') {
846            int8_t hi = ffHexCharToInt(strbuf->chars[read + 2]);
847            int8_t lo = ffHexCharToInt(strbuf->chars[read + 3]);
848            if (hi >= 0 && lo >= 0) {
849                strbuf->chars[write++] = (char) ((hi << 4) | lo);
850                read += 4;
851                changed = true;
852                continue;
853            }
854        }
855
856        strbuf->chars[write++] = strbuf->chars[read++];
857    }
858
859    strbuf->length = write;
860    strbuf->chars[write] = '\0';
861    return changed;
862}