summaryrefslogtreecommitdiffstats
path: root/src/common/impl/FFstrbuf.c
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/common/impl/FFstrbuf.c
Add the files
Diffstat (limited to 'src/common/impl/FFstrbuf.c')
-rw-r--r--src/common/impl/FFstrbuf.c903
1 files changed, 903 insertions, 0 deletions
diff --git a/src/common/impl/FFstrbuf.c b/src/common/impl/FFstrbuf.c
new file mode 100644
index 0000000..1f6477b
--- /dev/null
+++ b/src/common/impl/FFstrbuf.c
@@ -0,0 +1,903 @@
+#include "common/FFstrbuf.h"
+#include "common/mallocHelper.h"
+#include "common/strutil.h"
+
+#include <ctype.h>
+#include <inttypes.h>
+#include <math.h>
+
+char* CHAR_NULL_PTR = "";
+
+void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate) {
+ strbuf->allocated = allocate;
+
+ if (strbuf->allocated > 0) {
+ strbuf->chars = (char*) malloc(sizeof(char) * strbuf->allocated);
+ }
+
+ // This will set the length to zero and the null byte.
+ ffStrbufClear(strbuf);
+}
+
+void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
+ assert(format != NULL);
+
+ char* buffer = NULL;
+ int len = vasprintf(&buffer, format, arguments);
+ assert(len >= 0);
+
+ ffStrbufInitMoveNS(strbuf, (uint32_t) len, buffer);
+}
+
+// Takes ownership of `heapStr`. The caller must not free `heapStr` after calling this
+// function; the memory will be managed and freed via the associated FFstrbuf.
+void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr) {
+ assert(heapStr != NULL);
+
+ strbuf->length = length;
+ size_t allocSize = ffMallocUsableSize(heapStr);
+ if (allocSize == 0) {
+ allocSize = length + 1;
+ } else if (allocSize > UINT32_MAX) {
+ allocSize = UINT32_MAX;
+ }
+ strbuf->allocated = (uint32_t) allocSize;
+ strbuf->chars = heapStr;
+}
+
+void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free) {
+ if (ffStrbufGetFree(strbuf) >= free && !(strbuf->allocated == 0 && strbuf->length > 0)) {
+ return;
+ }
+
+ uint32_t allocate = strbuf->allocated;
+ if (allocate < FASTFETCH_STRBUF_DEFAULT_ALLOC) {
+ allocate = FASTFETCH_STRBUF_DEFAULT_ALLOC;
+ }
+
+ while ((strbuf->length + free + 1) > allocate) { // + 1 for the null byte
+ allocate *= 2;
+ }
+
+ if (strbuf->allocated == 0) {
+ char* newbuf = malloc(sizeof(*strbuf->chars) * allocate);
+ if (strbuf->length == 0) {
+ *newbuf = '\0';
+ } else {
+ memcpy(newbuf, strbuf->chars, strbuf->length + 1);
+ }
+ strbuf->chars = newbuf;
+ } else {
+ strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * allocate);
+ }
+
+ strbuf->allocated = allocate;
+}
+
+// Ensure that at least `free` bytes are available in the buffer besides the current length
+// for an empty buffer, free + 1 length memory will be allocated(+1 for the NUL)
+void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free) {
+ uint32_t oldFree = ffStrbufGetFree(strbuf);
+ if (oldFree >= free && !(strbuf->allocated == 0 && strbuf->length > 0)) {
+ return;
+ }
+
+ uint32_t newCap = strbuf->allocated + (free - oldFree);
+
+ if (strbuf->allocated == 0) {
+ newCap += strbuf->length + 1;
+ char* newbuf = malloc(sizeof(*strbuf->chars) * newCap);
+ if (strbuf->length == 0) {
+ *newbuf = '\0';
+ } else {
+ memcpy(newbuf, strbuf->chars, strbuf->length + 1);
+ }
+ strbuf->chars = newbuf;
+ } else {
+ strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * newCap);
+ }
+
+ strbuf->allocated = newCap;
+}
+
+void ffStrbufClear(FFstrbuf* strbuf) {
+ assert(strbuf != NULL);
+
+ if (strbuf->allocated == 0) {
+ strbuf->chars = CHAR_NULL_PTR;
+ } else {
+ strbuf->chars[0] = '\0';
+ }
+
+ strbuf->length = 0;
+}
+
+void ffStrbufAppendC(FFstrbuf* strbuf, char c) {
+ ffStrbufEnsureFree(strbuf, 1);
+ strbuf->chars[strbuf->length++] = c;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c) {
+ if (num == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, num);
+ memset(&strbuf->chars[strbuf->length], c, num);
+ strbuf->length += num;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
+ if (value == NULL || length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, length);
+ memcpy(&strbuf->chars[strbuf->length], value, length);
+ strbuf->length += length;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int)) {
+ if (value == NULL) {
+ return;
+ }
+
+ // Ensure capacity > 0 or the modification below will fail
+ uint32_t length = (uint32_t) strlen(value);
+ if (length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, length);
+ for (uint32_t i = 0; value[i] != '\0'; i++) {
+ strbuf->chars[strbuf->length++] = (char) transformFunc(value[i]);
+ }
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
+ assert(format != NULL);
+
+ va_list copy;
+ va_copy(copy, arguments);
+
+ uint32_t free = ffStrbufGetFree(strbuf);
+ int written = vsnprintf(strbuf->chars + strbuf->length, strbuf->allocated > 0 ? free + 1 : 0, format, arguments);
+
+ if (written > 0 && (uint32_t) written > free) {
+ ffStrbufEnsureFree(strbuf, (uint32_t) written);
+ written = vsnprintf(strbuf->chars + strbuf->length, (uint32_t) written + 1, format, copy);
+ }
+
+ va_end(copy);
+
+ if (written > 0) {
+ strbuf->length += (uint32_t) written;
+ }
+}
+
+const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until) {
+ if (value == NULL) {
+ return NULL;
+ }
+
+ const char* end = strchr(value, until);
+ if (end == NULL) {
+ ffStrbufAppendS(strbuf, value);
+ } else {
+ ffStrbufAppendNS(strbuf, (uint32_t) (end - value), value);
+ }
+ return end;
+}
+
+void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...) {
+ assert(format != NULL);
+
+ va_list arguments;
+ va_start(arguments, format);
+
+ if (strbuf->allocated == 0) {
+ ffStrbufInitVF(strbuf, format, arguments);
+ va_end(arguments);
+ return;
+ }
+
+ ffStrbufClear(strbuf);
+ ffStrbufAppendVF(strbuf, format, arguments);
+ va_end(arguments);
+}
+
+void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...) {
+ assert(format != NULL);
+
+ va_list arguments;
+ va_start(arguments, format);
+ ffStrbufAppendVF(strbuf, format, arguments);
+ va_end(arguments);
+}
+
+void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
+ if (value == NULL || length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, length);
+ memmove(strbuf->chars + length, strbuf->chars, strbuf->length + 1); // + 1 for the null byte
+ memcpy(strbuf->chars, value, length);
+ strbuf->length += length;
+}
+
+void ffStrbufPrependC(FFstrbuf* strbuf, char c) {
+ ffStrbufEnsureFree(strbuf, 1);
+ memmove(strbuf->chars + 1, strbuf->chars, strbuf->length + 1); // + 1 for the null byte
+ strbuf->chars[0] = c;
+ strbuf->length += 1;
+}
+
+void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
+ assert(strbuf != NULL);
+
+ if (length == 0) {
+ ffStrbufClear(strbuf);
+ return;
+ }
+
+ assert(value != NULL);
+
+ if (strbuf->allocated <= length) {
+ char* newBuf = malloc(sizeof(char) * (length + 1));
+ memcpy(newBuf, value, length);
+ if (strbuf->allocated > 0) {
+ free(strbuf->chars);
+ }
+ strbuf->chars = newBuf;
+ strbuf->allocated = length + 1;
+ } else {
+ memmove(strbuf->chars, value, length);
+ }
+
+ strbuf->length = length;
+ strbuf->chars[length] = '\0';
+}
+
+void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value) {
+ assert(value && value != strbuf);
+
+ if (value->length == 0) {
+ ffStrbufClear(strbuf);
+ return;
+ }
+
+ if (value->allocated == 0) // static string
+ {
+ if (strbuf->allocated != 0) {
+ free(strbuf->chars);
+ strbuf->allocated = 0;
+ }
+ strbuf->chars = value->chars;
+ strbuf->length = value->length;
+ return;
+ }
+ ffStrbufSetNS(strbuf, value->length, value->chars);
+}
+
+void ffStrbufTrimLeft(FFstrbuf* strbuf, char c) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ uint32_t index = 0;
+ while (index < strbuf->length && strbuf->chars[index] == c) {
+ ++index;
+ }
+
+ if (index == 0) {
+ return;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ strbuf->length -= index;
+ strbuf->chars += index;
+ return;
+ }
+
+ memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index);
+ strbuf->length -= index;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufTrimRight(FFstrbuf* strbuf, char c) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ if (!ffStrbufEndsWithC(strbuf, c)) {
+ return;
+ }
+
+ do {
+ --strbuf->length;
+ } while (ffStrbufEndsWithC(strbuf, c));
+
+ if (strbuf->allocated == 0) {
+ // static string
+ ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars);
+ return;
+ }
+
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufTrimLeftSpace(FFstrbuf* strbuf) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ uint32_t index = 0;
+ while (index < strbuf->length && isspace(strbuf->chars[index])) {
+ ++index;
+ }
+
+ if (index == 0) {
+ return;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ strbuf->length -= index;
+ strbuf->chars += index;
+ return;
+ }
+
+ memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index);
+ strbuf->length -= index;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufTrimRightSpace(FFstrbuf* strbuf) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ if (!ffStrbufEndsWithFn(strbuf, isspace)) {
+ return;
+ }
+
+ do {
+ --strbuf->length;
+ } while (ffStrbufEndsWithFn(strbuf, isspace));
+
+ if (strbuf->allocated == 0) {
+ // static string
+ ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars);
+ return;
+ }
+
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex) {
+ if (startIndex > strbuf->length || startIndex >= endIndex) {
+ return false;
+ }
+
+ if (endIndex > strbuf->length) {
+ ffStrbufSubstrBefore(strbuf, startIndex);
+ return true;
+ }
+
+ ffStrbufEnsureFree(strbuf, 0);
+ memmove(strbuf->chars + startIndex, strbuf->chars + endIndex, strbuf->length - endIndex);
+ strbuf->length -= (endIndex - startIndex);
+ strbuf->chars[strbuf->length] = '\0';
+ return true;
+}
+
+void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str) {
+ uint32_t stringLength = (uint32_t) strlen(str);
+
+ for (uint32_t i = ffStrbufNextIndexS(strbuf, 0, str); i < strbuf->length; i = ffStrbufNextIndexS(strbuf, i, str)) {
+ ffStrbufRemoveSubstr(strbuf, i, i + stringLength);
+ }
+}
+
+void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]) {
+ for (uint32_t i = 0; i < numStrings; i++) {
+ ffStrbufRemoveS(strbuf, strings[i]);
+ }
+}
+
+void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, 0);
+ for (
+ char* current_pos = memchr(strbuf->chars, find, strbuf->length);
+ current_pos;
+ current_pos = memchr(
+ current_pos + 1,
+ find,
+ strbuf->length - (uint32_t) (current_pos + 1 - strbuf->chars))) {
+ *current_pos = replace;
+ }
+}
+
+bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index) {
+ if (strbuf->length <= index) {
+ return false;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ if (index < strbuf->length) {
+ ffStrbufInitNS(strbuf, index, strbuf->chars);
+ }
+ return true;
+ }
+
+ strbuf->length = index;
+ strbuf->chars[strbuf->length] = '\0';
+ return true;
+}
+
+bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index) {
+ if (index >= strbuf->length) {
+ ffStrbufClear(strbuf);
+ return true;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ strbuf->length -= index + 1;
+ strbuf->chars += index + 1;
+ return true;
+ }
+
+ memmove(strbuf->chars, strbuf->chars + index + 1, strbuf->length - index - 1);
+ strbuf->length -= (index + 1);
+ strbuf->chars[strbuf->length] = '\0';
+ return true;
+}
+
+bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c) {
+ uint32_t index = ffStrbufFirstIndexC(strbuf, c);
+ if (index >= strbuf->length) {
+ return false;
+ }
+ ffStrbufSubstrAfter(strbuf, index);
+ return true;
+}
+
+bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str) {
+ if (*str == '\0') {
+ return false;
+ }
+
+ 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
+ if (index >= strbuf->length) {
+ return false;
+ }
+
+ ffStrbufSubstrAfter(strbuf, index);
+ return true;
+}
+
+bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c) {
+ uint32_t index = ffStrbufLastIndexC(strbuf, c);
+ if (index >= strbuf->length) {
+ return false;
+ }
+
+ ffStrbufSubstrAfter(strbuf, index);
+ return true;
+}
+
+bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end) {
+ if (__builtin_expect(start >= end, false)) {
+ ffStrbufClear(strbuf);
+ return false;
+ }
+
+ if (__builtin_expect(start == 0, false)) {
+ return ffStrbufSubstrBefore(strbuf, end);
+ }
+ if (__builtin_expect(end >= strbuf->length, false)) {
+ return ffStrbufSubstrAfter(strbuf, start - 1);
+ }
+
+ uint32_t len = end - start;
+ ffStrbufEnsureFixedLengthFree(strbuf, len); // In case of static string
+ memmove(strbuf->chars, strbuf->chars + start, len);
+
+ strbuf->length = len;
+ strbuf->chars[len] = '\0';
+ return true;
+}
+
+uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c) {
+ uint32_t result = 0;
+ for (uint32_t i = 0; i < strbuf->length; i++) {
+ if (strbuf->chars[i] == c) {
+ result++;
+ }
+ }
+
+ return result;
+}
+
+bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end) {
+ uint32_t endLength = (uint32_t) strlen(end);
+ if (ffStrbufEndsWithIgnCaseNS(strbuf, endLength, end)) {
+ ffStrbufSubstrBefore(strbuf, strbuf->length - endLength);
+ return true;
+ }
+
+ return false;
+}
+
+bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c) {
+ if (ffStrbufEndsWithC(strbuf, c)) {
+ return false;
+ }
+
+ ffStrbufAppendC(strbuf, c);
+ return true;
+}
+
+void ffStrbufWriteTo(const FFstrbuf* strbuf, FILE* file) {
+ fwrite(strbuf->chars, sizeof(*strbuf->chars), strbuf->length, file);
+}
+
+void ffStrbufPutTo(const FFstrbuf* strbuf, FILE* file) {
+ ffStrbufWriteTo(strbuf, file);
+ fputc('\n', file);
+}
+
+double ffStrbufToDouble(const FFstrbuf* strbuf, double defaultValue) {
+ char* str_end;
+ double result = strtod(strbuf->chars, &str_end);
+ return str_end == strbuf->chars ? defaultValue : result;
+}
+
+uint64_t ffStrbufToUInt(const FFstrbuf* strbuf, uint64_t defaultValue) {
+ char* str_end;
+ unsigned long long result = strtoull(strbuf->chars, &str_end, 10);
+ return str_end == strbuf->chars ? defaultValue : (uint64_t) result;
+}
+
+int64_t ffStrbufToSInt(const FFstrbuf* strbuf, int64_t defaultValue) {
+ char* str_end;
+ long long result = strtoll(strbuf->chars, &str_end, 10);
+ return str_end == strbuf->chars ? defaultValue : (int64_t) result;
+}
+
+void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value) {
+ ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number
+ char* start = strbuf->chars + strbuf->length;
+
+ yyjson_val val = {};
+ unsafe_yyjson_set_sint(&val, value);
+ char* end = yyjson_write_number(&val, start);
+
+ assert(end != NULL);
+
+ strbuf->length += (uint32_t) (end - start);
+}
+
+void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value) {
+ ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number
+ char* start = strbuf->chars + strbuf->length;
+
+ yyjson_val val = {};
+ unsafe_yyjson_set_uint(&val, value);
+ char* end = yyjson_write_number(&val, start);
+
+ assert(end != NULL);
+
+ strbuf->length += (uint32_t) (end - start);
+}
+
+void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros) {
+ assert(precision <= 15); // yyjson_write_number supports up to 15 digits after the decimal point
+
+ ffStrbufEnsureFree(strbuf, 40); // Required by yyjson_write_number
+ char* start = strbuf->chars + strbuf->length;
+
+ if (precision == 0) {
+ value = round(value);
+ }
+ yyjson_val val = {};
+ unsafe_yyjson_set_double(&val, value);
+ if (precision > 0) {
+ unsafe_yyjson_set_fp_to_fixed(&val, precision);
+ }
+
+ // Write at most <precision> digits after the decimal point; doesn't append trailing zeros
+ char* end = yyjson_write_number(&val, start);
+
+ assert(end > start);
+
+ strbuf->length += (uint32_t) (end - start);
+
+ if (__builtin_expect(value > 1e21 || value < -1e21, false)) {
+ // If the value is too large, yyjson_write_number will write it in scientific notation
+ return;
+ }
+
+ if (trailingZeros) {
+ if (precision > 1) {
+ for (char* p = end - 1; *p != '.' && p > start; --p) {
+ --precision;
+ }
+ if (precision > 0) {
+ ffStrbufAppendNC(strbuf, (uint32_t) precision, '0');
+ }
+ } else if (precision == 0 || (precision < 0 && end[-1] == '0')) {
+ goto removeDecimalPoint;
+ }
+ } else {
+ if (end[-1] == '0') {
+ removeDecimalPoint:
+ // yyjson always appends ".0" to make it a float point number. We need to remove it
+ strbuf->length -= 2;
+ strbuf->chars[strbuf->length] = '\0';
+ }
+ }
+}
+
+void ffStrbufUpperCase(FFstrbuf* strbuf) {
+ for (uint32_t i = 0; i < strbuf->length; ++i) {
+ strbuf->chars[i] = (char) toupper(strbuf->chars[i]);
+ }
+}
+
+void ffStrbufLowerCase(FFstrbuf* strbuf) {
+ for (uint32_t i = 0; i < strbuf->length; ++i) {
+ strbuf->chars[i] = (char) tolower(strbuf->chars[i]);
+ }
+}
+
+void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c) {
+ if (num == 0) {
+ return;
+ }
+ if (index >= strbuf->length) {
+ index = strbuf->length;
+ }
+
+ ffStrbufEnsureFree(strbuf, num);
+ memmove(strbuf->chars + index + num, strbuf->chars + index, strbuf->length - index + 1);
+ memset(&strbuf->chars[index], c, num);
+ strbuf->length += num;
+}
+
+bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
+ assert(lineptr && n && buffer);
+ assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
+ assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
+
+ const char* pBufferEnd = buffer->chars + buffer->length;
+ if (!*lineptr) {
+ *lineptr = buffer->chars;
+ } else {
+ *lineptr += *n;
+ if (*lineptr >= pBufferEnd) { // non-empty last line
+ return false;
+ }
+ **lineptr = delimiter;
+ ++*lineptr;
+ }
+ if (*lineptr >= pBufferEnd) { // empty last line
+ return false;
+ }
+
+ size_t remaining = (size_t) (pBufferEnd - *lineptr);
+ char* ending = memchr(*lineptr, delimiter, remaining);
+ if (ending) {
+ *n = (size_t) (ending - *lineptr);
+ *ending = '\0';
+ } else {
+ *n = remaining;
+ }
+ return true;
+}
+
+void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
+ assert(buffer && lineptr && n);
+ assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
+ assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
+
+ if (!*lineptr) {
+ return;
+ }
+
+ *lineptr += *n;
+ if (*lineptr < buffer->chars + buffer->length) {
+ **lineptr = delimiter;
+ }
+}
+
+bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf) {
+ if (strbuf->allocated == 0) {
+ return false; // Doesn't work with static strings
+ }
+
+ bool changed = false;
+ for (uint32_t i = 0; i < strbuf->length; i++) {
+ if (strbuf->chars[i] != ' ') {
+ continue;
+ }
+
+ i++;
+ uint32_t j = i;
+ for (; j < strbuf->length && strbuf->chars[j] == ' '; j++);
+
+ if (j == i) {
+ continue;
+ }
+ memmove(&strbuf->chars[i], &strbuf->chars[j], strbuf->length - j + 1);
+ strbuf->length -= j - i;
+ changed = true;
+ }
+
+ return changed;
+}
+
+/// @brief Check if a separated string (comp) contains a substring (strbuf).
+/// @param strbuf The substring to check.
+/// @param compLength The length of the separated string to check.
+/// @param comp The separated string to check.
+/// @param separator The separator character.
+bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ if (strbuf->length == 0) {
+ return true;
+ }
+
+ if (compLength == 0) {
+ return false;
+ }
+
+ for (const char* p = comp; p < comp + compLength;) {
+ const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
+ if (colon == NULL) {
+ uint32_t remainingLen = (uint32_t) (comp + compLength - p);
+ return strbuf->length == remainingLen && memcmp(strbuf->chars, p, remainingLen) == 0;
+ }
+
+ uint32_t substrLength = (uint32_t) (colon - p);
+ if (strbuf->length == substrLength && memcmp(strbuf->chars, p, substrLength) == 0) {
+ return true;
+ }
+
+ p = colon + 1;
+ }
+
+ return false;
+}
+
+/// @brief Case insensitive version of ffStrbufMatchSeparatedNS.
+bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ if (strbuf->length == 0) {
+ return true;
+ }
+
+ if (compLength == 0) {
+ return false;
+ }
+
+ for (const char* p = comp; p < comp + compLength;) {
+ const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
+ if (colon == NULL) {
+ uint32_t remainingLen = (uint32_t) (comp + compLength - p);
+ return strbuf->length == remainingLen && strncasecmp(strbuf->chars, p, remainingLen) == 0;
+ }
+
+ uint32_t substrLength = (uint32_t) (colon - p);
+ if (strbuf->length == substrLength && strncasecmp(strbuf->chars, p, substrLength) == 0) {
+ return true;
+ }
+
+ p = colon + 1;
+ }
+
+ return false;
+}
+
+int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint) {
+ if (codepoint <= 0x7F) {
+ ffStrbufAppendC(strbuf, (char) codepoint);
+ return 1;
+ } else if (codepoint <= 0x7FF) {
+ ffStrbufAppendNS(strbuf, 2, (char[]) { (char) (0xC0 | (codepoint >> 6)), (char) (0x80 | (codepoint & 0x3F)) });
+ return 2;
+ } else if (codepoint <= 0xFFFF) {
+ ffStrbufAppendNS(strbuf, 3, (char[]) { (char) (0xE0 | (codepoint >> 12)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) });
+ return 3;
+ } else if (codepoint <= 0x10FFFF) {
+ ffStrbufAppendNS(strbuf, 4, (char[]) { (char) (0xF0 | (codepoint >> 18)), (char) (0x80 | ((codepoint >> 12) & 0x3F)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) });
+ return 4;
+ }
+
+ ffStrbufAppendS(strbuf, "�"); // U+FFFD REPLACEMENT CHARACTER
+ return 1;
+}
+
+/// @brief Check if a separated string (strbuf) contains a substring (comp).
+/// @param strbuf The separated to check.
+/// @param compLength The length of the separated string to check.
+/// @param comp The substring to check.
+/// @param separator The separator character.
+bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ uint32_t startIndex = 0;
+ while (startIndex < strbuf->length) {
+ uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
+
+ uint32_t folderLength = colonIndex - startIndex;
+ if (folderLength == compLength && memcmp(strbuf->chars + startIndex, comp, compLength) == 0) {
+ return true;
+ }
+
+ startIndex = colonIndex + 1;
+ }
+
+ return false;
+}
+
+bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ uint32_t startIndex = 0;
+ while (startIndex < strbuf->length) {
+ uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
+
+ uint32_t folderLength = colonIndex - startIndex;
+ if (folderLength == compLength && strncasecmp(strbuf->chars + startIndex, comp, compLength) == 0) {
+ return true;
+ }
+
+ startIndex = colonIndex + 1;
+ }
+
+ return false;
+}
+
+bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf) {
+ assert(strbuf);
+
+ if (strbuf->length < 4) {
+ return false;
+ }
+
+ // Static string must be converted first.
+ assert(strbuf->allocated > 0);
+
+ bool changed = false;
+ uint32_t read = 0;
+ uint32_t write = 0;
+
+ while (read < strbuf->length) {
+ if (
+ read + 3 < strbuf->length &&
+ strbuf->chars[read] == '\\' &&
+ strbuf->chars[read + 1] == 'x') {
+ int8_t hi = ffHexCharToInt(strbuf->chars[read + 2]);
+ int8_t lo = ffHexCharToInt(strbuf->chars[read + 3]);
+ if (hi >= 0 && lo >= 0) {
+ strbuf->chars[write++] = (char) ((hi << 4) | lo);
+ read += 4;
+ changed = true;
+ continue;
+ }
+ }
+
+ strbuf->chars[write++] = strbuf->chars[read++];
+ }
+
+ strbuf->length = write;
+ strbuf->chars[write] = '\0';
+ return changed;
+}