master ohmu/fastfetch-openbsd / src / common / FFstrbuf.h
ohmu  ·  2026-10-15
  1#pragma once
  2
  3#include <stdint.h>
  4#include <stdarg.h>
  5#include <stdbool.h>
  6#include <stdio.h>
  7#include <string.h>
  8#include <stdlib.h>
  9#include <assert.h>
 10#include "common/memrchr.h"
 11#include "common/arrutil.h"
 12
 13#ifdef FF_USE_SYSTEM_YYJSON
 14    #include <yyjson.h>
 15#else
 16    #include "3rdparty/yyjson/yyjson.h"
 17#endif
 18
 19#ifdef _WIN32
 20// #include <shlwapi.h>
 21__stdcall char* StrStrIA(const char* lpFirst, const char* lpSrch);
 22    #define strcasestr StrStrIA
 23#endif
 24
 25#define FASTFETCH_STRBUF_DEFAULT_ALLOC 32
 26
 27// ---------------------------------------------------------------------------------------------
 28// Contract attributes used throughout this header:
 29//
 30// `gnu::pure` marks a function that reads memory (through its arguments and through globals) but
 31// writes nothing observable, so the compiler may cache and reorder calls to it. It is applied to
 32// the comparison / search accessors, and deliberately NOT to:
 33//   * the `To*` converters -- `strtod` / `strtoull` / `strtoll` write `errno`;
 34//   * `ffStrbufEndsWithFn` -- the caller-supplied `fn` may have side effects;
 35//   * `ffStrbufWriteTo` / `ffStrbufPutTo` -- they write to a `FILE*`.
 36//
 37// `gnu::nonnull(N)` is added wherever the function dereferences argument N unconditionally. The
 38// two cannot be combined with a null check: clang reports `-Wtautological-pointer-compare` /
 39// `-Wpointer-bool-conversion` (both in `-Wall`) when a `nonnull` parameter is compared against
 40// null, so a redundant `assert(p != nullptr)` is dropped in favour of the attribute. Asserts that
 41// state something `nonnull` cannot express -- pointer aliasing (`value != strbuf`), index ranges
 42// (`start <= strbuf->length`), or a non-pointer invariant -- are kept.
 43//
 44// `nodiscard` is NOT applied to the "was the buffer modified" boolean returns
 45// (`ffStrbufSubstr*`, `ffStrbufRemoveSubstr`, `ffStrbufEnsureEndsWithC`, ...). Ignoring that
 46// result is a normal, intended use of those functions -- `ffStrbufSubstrBefore` alone has 153 call
 47// sites and ~147 of them discard the result -- so `nodiscard` would only produce noise.
 48// ---------------------------------------------------------------------------------------------
 49
 50// static string (allocated == 0), chars points to a string literal
 51// dynamic string (allocated > 0), chars points to a heap allocated buffer
 52typedef struct FFstrbuf {
 53    uint32_t allocated;
 54    uint32_t length;
 55    char* chars;
 56} FFstrbuf;
 57
 58[[gnu::nonnull(1)]] static inline void ffStrbufInit(FFstrbuf* strbuf);
 59[[gnu::nonnull(1)]] void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate);
 60[[gnu::nonnull(1, 2), gnu::format(printf, 2, 0)]] void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments);
 61[[gnu::nonnull(1, 3)]] void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr);
 62[[gnu::format(printf, 2, 3), gnu::nonnull(1, 2)]] void ffStrbufInitF(FFstrbuf* strbuf, const char* format, ...);
 63[[gnu::format(printf, 1, 2), gnu::nonnull(1), nodiscard]] FFstrbuf ffStrbufCreateF(const char* format, ...);
 64
 65[[gnu::nonnull(1)]] void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free);
 66[[gnu::nonnull(1)]] void ffStrbufEnsureFreeNoCheck(FFstrbuf* strbuf, uint32_t free);
 67
 68[[gnu::nonnull(1)]] static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value);
 69[[gnu::nonnull(1, 3)]] void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int));
 70[[gnu::format(printf, 2, 3), gnu::nonnull(1, 2)]] void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...);
 71[[gnu::nonnull(1, 2), gnu::format(printf, 2, 0)]] void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments);
 72// Returns the pointer to the terminator, or nullptr if `value` was nullptr; callers are free to
 73// ignore it, so not `nodiscard`.
 74[[gnu::nonnull(1)]] const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until);
 75
 76[[gnu::nonnull(1)]] void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value);
 77[[gnu::nonnull(1)]] void ffStrbufPrependC(FFstrbuf* strbuf, char c);
 78
 79[[gnu::nonnull(1)]] void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c);
 80
 81// Clear the content of strbuf and set new value
 82// NOTE: Unlike ffStrbufAppend*, ffStrbufSet* functions may NOT reserve extra space
 83[[gnu::nonnull(1, 2)]] void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value);
 84[[gnu::nonnull(1, 3)]] void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value);
 85[[gnu::format(printf, 2, 3), gnu::nonnull(1, 2)]] void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...);
 86
 87[[gnu::nonnull(1)]] void ffStrbufTrimLeft(FFstrbuf* strbuf, char c);
 88[[gnu::nonnull(1)]] void ffStrbufTrimRight(FFstrbuf* strbuf, char c);
 89[[gnu::nonnull(1)]] void ffStrbufTrimLeftSpace(FFstrbuf* strbuf);
 90[[gnu::nonnull(1)]] void ffStrbufTrimRightSpace(FFstrbuf* strbuf);
 91
 92[[gnu::nonnull(1)]] bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex);
 93[[gnu::nonnull(1, 2)]] void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str);
 94// `strings` is only dereferenced when `numStrings > 0`, so it is intentionally not `nonnull(3)`
 95[[gnu::nonnull(1)]] void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]);
 96
 97[[gnu::nonnull(1)]] void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace);
 98
 99// Returns true if the strbuf is modified
100[[gnu::nonnull(1)]] bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index);
101[[gnu::nonnull(1)]] bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index); // Not including the index
102[[gnu::nonnull(1)]] bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c);
103[[gnu::nonnull(1, 2)]] bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str);
104[[gnu::nonnull(1)]] bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c);
105[[gnu::nonnull(1)]] bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end);
106
107[[gnu::nonnull(1), gnu::pure, nodiscard]] uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c);
108
109[[gnu::nonnull(1, 2)]] bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end);
110
111[[gnu::nonnull(1)]] bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c);
112
113[[gnu::nonnull(1)]] void ffStrbufUpperCase(FFstrbuf* strbuf);
114[[gnu::nonnull(1)]] void ffStrbufLowerCase(FFstrbuf* strbuf);
115
116// Function alters the buffer to extract lines or delimited segments (replaces the delimiter with '\0')
117// so that buffer MUST be heap allocated (NOT a static string)
118// `lineptr` must be `nullptr` and `n` MUST be `0` for the first call
119// Caller MUST NOT free `*lineptr`
120[[gnu::nonnull(1, 2, 4)]] bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
121[[gnu::nonnull(1, 2, 4)]] void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
122
123/**
124 * @brief Read a line from a FFstrbuf.
125 *
126 * @details Behaves like getline(3) but reads from a FFstrbuf.
127 *
128 * @param[in,out] lineptr The pointer to a pointer that will be set to the start of the line
129                          (points to buffer's internal memory address to avoid memory allocation and copy).
130                          MUST NOT be freed by the caller, unlike `getline(3)`.
131 *                        MUST be nullptr for the first call.
132 * @param[in,out] n The pointer to the size of the buffer of lineptr.
133                    MUST be 0 for the first call.
134 * @param[in] buffer The buffer to read from.
135                     MUST be heap allocated (NOT a static string).
136 *
137 * @return true if a line has been read, false if the end of the buffer has been reached.
138 */
139[[gnu::nonnull(1, 2, 3), nodiscard]] static inline bool ffStrbufGetline(char** lineptr, size_t* n, FFstrbuf* buffer) {
140    return ffStrbufGetdelim(lineptr, n, '\n', buffer);
141}
142/**
143 * @brief Restore the end of a line that was modified by ffStrbufGetline.
144 * @warning This function should be called before breaking an ffStrbufGetline loop if `buffer` will be used later.
145 */
146[[gnu::nonnull(1, 2, 3)]] static inline void ffStrbufGetlineRestore(char** lineptr, size_t* n, FFstrbuf* buffer) {
147    ffStrbufGetdelimRestore(lineptr, n, '\n', buffer);
148}
149[[gnu::nonnull(1)]] bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf);
150// `comp` is only dereferenced when `compLength > 0` in the first pair, and is never dereferenced
151// when `strbuf` is empty in the second pair, so neither takes `nonnull(3)`
152[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
153[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
154[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
155[[gnu::nonnull(1), gnu::pure, nodiscard]] bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
156
157[[gnu::nonnull(1)]] int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint);
158
159[[gnu::nonnull(1)]] void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value);
160[[gnu::nonnull(1)]] void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value);
161// Appends a double value to the string buffer with the specified precision (0~15).
162// if `precision < 0`, let yyjson decide the precision
163[[gnu::nonnull(1)]] void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros);
164
165[[nodiscard]] static inline FFstrbuf ffStrbufCreateA(uint32_t allocate) {
166    FFstrbuf strbuf;
167    ffStrbufInitA(&strbuf, allocate);
168    return strbuf;
169}
170
171[[gnu::nonnull(1, 2)]] static inline void ffStrbufInitCopy(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict src) {
172    if (src->allocated == 0) { // static string
173        *strbuf = *src;
174    } else {
175        ffStrbufInitA(strbuf, src->allocated);
176        ffStrbufAppend(strbuf, src);
177    }
178}
179
180[[gnu::nonnull(1), nodiscard]] static inline FFstrbuf ffStrbufCreateCopy(const FFstrbuf* src) {
181    FFstrbuf strbuf;
182    ffStrbufInitCopy(&strbuf, src);
183    return strbuf;
184}
185
186// Move the content of `src` into `strbuf`, and left `src` empty
187[[gnu::nonnull(1)]] static inline void ffStrbufInitMove(FFstrbuf* strbuf, FFstrbuf* src) {
188    if (src) {
189        *strbuf = *src;
190        ffStrbufInit(src);
191    } else {
192        ffStrbufInit(strbuf);
193    }
194}
195
196[[nodiscard]] static inline FFstrbuf ffStrbufCreateMove(FFstrbuf* src) {
197    FFstrbuf strbuf;
198    ffStrbufInitMove(&strbuf, src);
199    return strbuf;
200}
201
202[[gnu::nonnull(1, 2)]] static inline void ffStrbufInitMoveS(FFstrbuf* strbuf, char* heapStr) {
203    ffStrbufInitMoveNS(strbuf, (uint32_t) strlen(heapStr), heapStr);
204}
205
206// Despite the name, this function resets strbuf to the initial/unallocated state
207[[gnu::nonnull(1)]] static inline void ffStrbufDestroy(FFstrbuf* strbuf) {
208    if (strbuf->allocated > 0) {
209        free(strbuf->chars);
210    }
211
212    ffStrbufInit(strbuf);
213}
214
215[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufGetFree(const FFstrbuf* strbuf) {
216    if (strbuf->allocated == 0) {
217        return 0;
218    }
219
220    return strbuf->allocated - strbuf->length - 1; // - 1 for the null byte
221}
222
223[[gnu::nonnull(1)]] static inline void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free) {
224    if (__builtin_expect(free == 0, false)) {
225        if (__builtin_expect(!(strbuf->allocated == 0 && strbuf->length > 0), true)) {
226            return;
227        }
228    } else {
229        if (__builtin_expect(ffStrbufGetFree(strbuf) >= free, true)) {
230            return;
231        }
232    }
233
234    ffStrbufEnsureFreeNoCheck(strbuf, free);
235}
236
237
238[[gnu::nonnull(1)]] static inline void ffStrbufClear(FFstrbuf* strbuf) {
239    extern char* CHAR_NULL_PTR;
240
241    if (strbuf->allocated == 0) {
242        strbuf->chars = CHAR_NULL_PTR;
243    } else {
244        strbuf->chars[0] = '\0';
245    }
246
247    strbuf->length = 0;
248}
249
250[[gnu::nonnull(1)]] static inline void ffStrbufAppendC(FFstrbuf* strbuf, char c) {
251    ffStrbufEnsureFree(strbuf, 1);
252    strbuf->chars[strbuf->length++] = c;
253    strbuf->chars[strbuf->length] = '\0';
254}
255
256[[gnu::nonnull(1)]] static inline void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c) {
257    if (__builtin_expect(num == 0, false)) {
258        return;
259    }
260    ffStrbufEnsureFree(strbuf, num);
261
262    memset(&strbuf->chars[strbuf->length], c, num);
263    strbuf->length += num;
264    strbuf->chars[strbuf->length] = '\0';
265}
266
267[[gnu::nonnull(1)]] static inline void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
268    if (__builtin_expect(value == nullptr || length == 0, false)) {
269        return;
270    }
271    ffStrbufEnsureFree(strbuf, length);
272
273    memcpy(&strbuf->chars[strbuf->length], value, length);
274    strbuf->length += length;
275    strbuf->chars[strbuf->length] = '\0';
276}
277
278[[gnu::nonnull(1)]] static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value) {
279    assert(value != strbuf);
280    if (value == nullptr) {
281        return;
282    }
283    ffStrbufAppendNS(strbuf, value->length, value->chars);
284}
285
286[[gnu::nonnull(1)]] static inline void ffStrbufRecalculateLength(FFstrbuf* strbuf) {
287    strbuf->length = (uint32_t) strlen(strbuf->chars);
288}
289
290// `value` may be null (clears the buffer); `strbuf` may not
291[[gnu::nonnull(1)]] static inline void ffStrbufSetS(FFstrbuf* strbuf, const char* value) {
292    if (value == nullptr) {
293        ffStrbufClear(strbuf);
294    } else {
295        ffStrbufSetNS(strbuf, (uint32_t) strlen(value), value);
296    }
297}
298
299[[gnu::nonnull(1)]] static inline bool ffStrbufSetJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
300    if (yyjson_is_str(jsonVal)) {
301        ffStrbufSetNS(strbuf, (uint32_t) unsafe_yyjson_get_len(jsonVal), unsafe_yyjson_get_str(jsonVal));
302        return true;
303    }
304
305    ffStrbufClear(strbuf);
306    return false;
307}
308
309[[gnu::nonnull(1)]] static inline void ffStrbufAppendS(FFstrbuf* strbuf, const char* value) {
310    if (value == nullptr) {
311        return;
312    }
313    ffStrbufAppendNS(strbuf, (uint32_t) strlen(value), value);
314}
315
316// Returns whether `jsonVal` was a string. Callers routinely pre-check with `yyjson_is_str`, so the
317// result is not `nodiscard`.
318static inline bool ffStrbufAppendJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
319    if (yyjson_is_str(jsonVal)) {
320        ffStrbufAppendNS(strbuf, (uint32_t) unsafe_yyjson_get_len(jsonVal), unsafe_yyjson_get_str(jsonVal));
321        return true;
322    }
323    return false;
324}
325
326[[gnu::nonnull(1)]] static inline void ffStrbufInit(FFstrbuf* strbuf) {
327    extern char* CHAR_NULL_PTR;
328    strbuf->allocated = strbuf->length = 0;
329    strbuf->chars = CHAR_NULL_PTR;
330}
331
332[[nodiscard]] static inline FFstrbuf ffStrbufCreate(void) {
333    FFstrbuf strbuf;
334    ffStrbufInit(&strbuf);
335    return strbuf;
336}
337
338static inline void ffStrbufInitStatic(FFstrbuf* strbuf, const char* str) {
339    ffStrbufInit(strbuf);
340    if (!str) {
341        return;
342    }
343
344    strbuf->allocated = 0;
345    strbuf->length = (uint32_t) strlen(str);
346    strbuf->chars = (char*) str;
347}
348
349[[nodiscard]] static inline FFstrbuf ffStrbufCreateStatic(const char* str) {
350    FFstrbuf strbuf;
351    ffStrbufInitStatic(&strbuf, str);
352    return strbuf;
353}
354
355[[gnu::nonnull(1)]] static inline void ffStrbufSetStatic(FFstrbuf* strbuf, const char* value) {
356    if (strbuf->allocated > 0) {
357        free(strbuf->chars);
358    }
359
360    if (value != nullptr) {
361        ffStrbufInitStatic(strbuf, value);
362    } else {
363        ffStrbufInit(strbuf);
364    }
365}
366
367[[gnu::nonnull(1)]] static inline void ffStrbufInitNS(FFstrbuf* strbuf, uint32_t length, const char* str) {
368    ffStrbufInit(strbuf);
369    ffStrbufAppendNS(strbuf, length, str);
370}
371
372[[nodiscard]] static inline FFstrbuf ffStrbufCreateNS(uint32_t length, const char* str) {
373    FFstrbuf strbuf;
374    ffStrbufInitNS(&strbuf, length, str);
375    return strbuf;
376}
377
378// Returns whether `jsonVal` was a string; not `nodiscard`, same reason as ffStrbufAppendJsonVal
379static inline bool ffStrbufInitJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
380    ffStrbufInit(strbuf);
381    return ffStrbufAppendJsonVal(strbuf, jsonVal);
382}
383
384[[gnu::nonnull(1)]] static inline void ffStrbufInitS(FFstrbuf* strbuf, const char* str) {
385    ffStrbufInit(strbuf);
386    ffStrbufAppendS(strbuf, str);
387}
388
389[[nodiscard]] static inline FFstrbuf ffStrbufCreateS(const char* str) {
390    FFstrbuf strbuf;
391    ffStrbufInitS(&strbuf, str);
392    return strbuf;
393}
394
395[[gnu::nonnull(1)]] static inline void ffStrbufPrepend(FFstrbuf* strbuf, FFstrbuf* value) {
396    if (value == nullptr) {
397        return;
398    }
399    ffStrbufPrependNS(strbuf, value->length, value->chars);
400}
401
402[[gnu::nonnull(1)]] static inline void ffStrbufPrependS(FFstrbuf* strbuf, const char* value) {
403    if (value == nullptr) {
404        return;
405    }
406    ffStrbufPrependNS(strbuf, (uint32_t) strlen(value), value);
407}
408
409[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
410    uint32_t length = strbuf->length > comp->length ? comp->length : strbuf->length;
411    return memcmp(strbuf->chars, comp->chars, length + 1);
412}
413
414[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
415    return ffStrbufComp(strbuf, comp) == 0;
416}
417
418[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufCompS(const FFstrbuf* strbuf, const char* comp) {
419    return strcmp(strbuf->chars, comp);
420}
421
422[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEqualS(const FFstrbuf* strbuf, const char* comp) {
423    return ffStrbufCompS(strbuf, comp) == 0;
424}
425
426[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufIgnCaseCompS(const FFstrbuf* strbuf, const char* comp) {
427    return strcasecmp(strbuf->chars, comp);
428}
429
430[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufIgnCaseEqualS(const FFstrbuf* strbuf, const char* comp) {
431    return ffStrbufIgnCaseCompS(strbuf, comp) == 0;
432}
433
434[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline int ffStrbufIgnCaseComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
435    return ffStrbufIgnCaseCompS(strbuf, comp->chars);
436}
437
438[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufIgnCaseEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
439    return ffStrbufIgnCaseComp(strbuf, comp) == 0;
440}
441
442[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufContainC(const FFstrbuf* strbuf, char c) {
443    return memchr(strbuf->chars, c, strbuf->length) != nullptr;
444}
445
446[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContainS(const FFstrbuf* strbuf, const char* str) {
447    return strstr(strbuf->chars, str) != nullptr;
448}
449
450[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContain(const FFstrbuf* strbuf, const FFstrbuf* str) {
451    return ffStrbufContainS(strbuf, str->chars);
452}
453
454[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContainIgnCaseS(const FFstrbuf* strbuf, const char* str) {
455    return strcasestr(strbuf->chars, str) != nullptr;
456}
457
458[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* str) {
459    return ffStrbufContainIgnCaseS(strbuf, str->chars);
460}
461
462[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufNextIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
463    assert(start <= strbuf->length);
464
465    const char* ptr = (const char*) memchr(strbuf->chars + start, c, strbuf->length - start);
466    return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
467}
468
469[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufContainAnyC(const FFstrbuf* strbuf, const char* cs) {
470    assert(cs != nullptr);
471
472    return strpbrk(strbuf->chars, cs) != nullptr;
473}
474
475[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline uint32_t ffStrbufNextIndexS(const FFstrbuf* strbuf, uint32_t start, const char* str) {
476    assert(start <= strbuf->length);
477
478    const char* ptr = strstr(strbuf->chars + start, str);
479    return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
480}
481
482[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufPreviousIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
483    assert(start <= strbuf->length);
484
485    const char* ptr = (const char*) memrchr(strbuf->chars, c, start + 1);
486    return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
487}
488
489[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufFirstIndexC(const FFstrbuf* strbuf, char c) {
490    return ffStrbufNextIndexC(strbuf, 0, c);
491}
492
493[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline uint32_t ffStrbufFirstIndex(const FFstrbuf* strbuf, const FFstrbuf* searched) {
494    return ffStrbufNextIndexS(strbuf, 0, searched->chars);
495}
496
497[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline uint32_t ffStrbufFirstIndexS(const FFstrbuf* strbuf, const char* str) {
498    return ffStrbufNextIndexS(strbuf, 0, str);
499}
500
501[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline uint32_t ffStrbufLastIndexC(const FFstrbuf* strbuf, char c) {
502    if (strbuf->length == 0) {
503        return 0;
504    }
505
506    return ffStrbufPreviousIndexC(strbuf, strbuf->length - 1, c);
507}
508
509[[gnu::nonnull(1)]] static inline bool ffStrbufSubstrBeforeFirstC(FFstrbuf* strbuf, char c) {
510    return ffStrbufSubstrBefore(strbuf, ffStrbufFirstIndexC(strbuf, c));
511}
512
513[[gnu::nonnull(1)]] static inline bool ffStrbufSubstrBeforeLastC(FFstrbuf* strbuf, char c) {
514    return ffStrbufSubstrBefore(strbuf, ffStrbufLastIndexC(strbuf, c));
515}
516
517[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithC(const FFstrbuf* strbuf, char c) {
518    return strbuf->chars[0] == c;
519}
520
521[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithSN(const FFstrbuf* strbuf, const char* start, uint32_t length) {
522    if (length > strbuf->length) {
523        return false;
524    }
525
526    return memcmp(strbuf->chars, start, length) == 0;
527}
528
529[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithS(const FFstrbuf* strbuf, const char* start) {
530    return ffStrbufStartsWithSN(strbuf, start, (uint32_t) strlen(start));
531}
532
533[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWith(const FFstrbuf* strbuf, const FFstrbuf* start) {
534    return ffStrbufStartsWithSN(strbuf, start->chars, start->length);
535}
536
537[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t length, const char* start) {
538    if (length > strbuf->length) {
539        return false;
540    }
541    return strncasecmp(strbuf->chars, start, length) == 0;
542}
543
544[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithIgnCaseS(const FFstrbuf* strbuf, const char* start) {
545    return ffStrbufStartsWithIgnCaseNS(strbuf, (uint32_t) strlen(start), start);
546}
547
548[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufStartsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* start) {
549    return ffStrbufStartsWithIgnCaseNS(strbuf, start->length, start->chars);
550}
551
552[[gnu::nonnull(1), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithC(const FFstrbuf* strbuf, char c) {
553    return strbuf->length == 0 ? false : strbuf->chars[strbuf->length - 1] == c;
554}
555
556[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
557    if (endLength > strbuf->length) {
558        return false;
559    }
560
561    return memcmp(strbuf->chars + strbuf->length - endLength, end, endLength) == 0;
562}
563
564[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithS(const FFstrbuf* strbuf, const char* end) {
565    return ffStrbufEndsWithNS(strbuf, (uint32_t) strlen(end), end);
566}
567
568// Not `pure`: the caller-supplied `fn` may have side effects
569[[gnu::nonnull(1, 2), nodiscard]] static inline bool ffStrbufEndsWithFn(const FFstrbuf* strbuf, int (*const fn)(int)) {
570    return strbuf->length == 0 ? false : fn(strbuf->chars[strbuf->length - 1]);
571}
572
573[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWith(const FFstrbuf* strbuf, const FFstrbuf* end) {
574    return ffStrbufEndsWithNS(strbuf, end->length, end->chars);
575}
576
577[[gnu::nonnull(1, 3), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
578    if (endLength > strbuf->length) {
579        return false;
580    }
581    return strcasecmp(strbuf->chars + strbuf->length - endLength, end) == 0;
582}
583
584[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithIgnCaseS(const FFstrbuf* strbuf, const char* end) {
585    return ffStrbufEndsWithIgnCaseNS(strbuf, (uint32_t) strlen(end), end);
586}
587
588[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufEndsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* end) {
589    return ffStrbufEndsWithIgnCaseNS(strbuf, end->length, end->chars);
590}
591
592[[gnu::nonnull(1)]] static inline void ffStrbufTrim(FFstrbuf* strbuf, char c) {
593    ffStrbufTrimRight(strbuf, c);
594    ffStrbufTrimLeft(strbuf, c);
595}
596
597[[gnu::nonnull(1)]] static inline void ffStrbufTrimSpace(FFstrbuf* strbuf) {
598    ffStrbufTrimRightSpace(strbuf);
599    ffStrbufTrimLeftSpace(strbuf);
600}
601
602[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparatedS(const FFstrbuf* strbuf, const char* comp, char separator) {
603    return ffStrbufMatchSeparatedNS(strbuf, (uint32_t) strlen(comp), comp, separator);
604}
605
606[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparated(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
607    return ffStrbufMatchSeparatedNS(strbuf, comp->length, comp->chars, separator);
608}
609
610[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparatedIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
611    return ffStrbufMatchSeparatedIgnCaseNS(strbuf, (uint32_t) strlen(comp), comp, separator);
612}
613
614[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufMatchSeparatedIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
615    return ffStrbufMatchSeparatedIgnCaseNS(strbuf, comp->length, comp->chars, separator);
616}
617
618[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContainS(const FFstrbuf* strbuf, const char* comp, char separator) {
619    return ffStrbufSeparatedContainNS(strbuf, (uint32_t) strlen(comp), comp, separator);
620}
621
622[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContain(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
623    return ffStrbufSeparatedContainNS(strbuf, comp->length, comp->chars, separator);
624}
625
626[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContainIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
627    return ffStrbufSeparatedContainIgnCaseNS(strbuf, (uint32_t) strlen(comp), comp, separator);
628}
629
630[[gnu::nonnull(1, 2), gnu::pure, nodiscard]] static inline bool ffStrbufSeparatedContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
631    return ffStrbufSeparatedContainIgnCaseNS(strbuf, comp->length, comp->chars, separator);
632}
633
634[[gnu::nonnull(1, 2)]] static inline void ffStrbufWriteTo(const FFstrbuf* strbuf, FILE* file) {
635    fwrite(strbuf->chars, sizeof(*strbuf->chars), strbuf->length, file);
636}
637
638[[gnu::nonnull(1, 2)]] static inline void ffStrbufPutTo(const FFstrbuf* strbuf, FILE* file) {
639    ffStrbufWriteTo(strbuf, file);
640    fputc('\n', file);
641}
642
643// Not `pure`: `strtod` reads the LC_NUMERIC locale and writes errno
644[[gnu::nonnull(1), nodiscard]] static inline double ffStrbufToDouble(const FFstrbuf* strbuf, double defaultValue) {
645    char* str_end;
646    double result = strtod(strbuf->chars, &str_end);
647    return str_end == strbuf->chars ? defaultValue : result;
648}
649
650// Not `pure`: `strtoull` reads the LC_NUMERIC locale and writes errno
651[[gnu::nonnull(1), nodiscard]] static inline uint64_t ffStrbufToUInt(const FFstrbuf* strbuf, uint64_t defaultValue) {
652    char* str_end;
653    unsigned long long result = strtoull(strbuf->chars, &str_end, 10);
654    return str_end == strbuf->chars ? defaultValue : (uint64_t) result;
655}
656
657// Not `pure`: `strtoll` reads the LC_NUMERIC locale and writes errno
658[[gnu::nonnull(1), nodiscard]] static inline int64_t ffStrbufToSInt(const FFstrbuf* strbuf, int64_t defaultValue) {
659    char* str_end;
660    long long result = strtoll(strbuf->chars, &str_end, 10);
661    return str_end == strbuf->chars ? defaultValue : (int64_t) result;
662}
663
664// Returns true if the strbuf is modified
665[[gnu::nonnull(1)]] [[gnu::nonnull(1)]] bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf);
666
667#define FF_STRBUF_AUTO_DESTROY [[gnu::cleanup(ffStrbufDestroy)]] FFstrbuf
668#define FF_STRBUF_STATIC(str) { .allocated = 0, .length = (uint32_t) sizeof(str) - 1, .chars = str }