From fbf79ac6fe3868ee6c428f1d962f44cc7b680771 Mon Sep 17 00:00:00 2001 From: Carter Li Date: Tue, 15 Sep 2026 10:33:00 +0800 Subject: [PATCH] FFstrbuf: improves empty-string handling for ffStrbufMatchSeparatedNS and ffStrbufSeparatedContainNS Fixes #2585 --- src/common/impl/FFstrbuf.c | 39 +++++++++++++++++++-------------- tests/strbuf.c | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/common/impl/FFstrbuf.c b/src/common/impl/FFstrbuf.c index 27cb4d66a..20693c1ec 100644 --- a/src/common/impl/FFstrbuf.c +++ b/src/common/impl/FFstrbuf.c @@ -723,16 +723,17 @@ bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf) { /// @param compLength The length of the separated string to check. /// @param comp The separated string to check. /// @param separator The separator character. +/// +/// Empty-string handling: +/// - If `strbuf` is empty, it matches only if `comp` contains an empty segment +/// (e.g. "abc::def", "abc:", ":abc") or `comp` itself is empty. +/// - If `comp` is empty (compLength == 0), it matches only if `strbuf` is also empty. bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) { - if (strbuf->length == 0) { - return true; + if (__builtin_expect(compLength == 0, false)) { + return strbuf->length == 0; } - if (compLength == 0) { - return false; - } - - for (const char* p = comp; p < comp + compLength;) { + for (const char* p = comp; p <= comp + compLength;) { const char* colon = memchr(p, separator, (size_t) (comp + compLength - p)); if (colon == nullptr) { uint32_t remainingLen = (uint32_t) (comp + compLength - p); @@ -752,15 +753,11 @@ bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const /// @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 (__builtin_expect(compLength == 0, false)) { + return strbuf->length == 0; } - if (compLength == 0) { - return false; - } - - for (const char* p = comp; p < comp + compLength;) { + for (const char* p = comp; p <= comp + compLength;) { const char* colon = memchr(p, separator, (size_t) (comp + compLength - p)); if (colon == nullptr) { uint32_t remainingLen = (uint32_t) (comp + compLength - p); @@ -802,9 +799,15 @@ int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint) { /// @param compLength The length of the separated string to check. /// @param comp The substring to check. /// @param separator The separator character. +/// +/// Empty-string handling is symmetric to ffStrbufMatchSeparatedNS bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) { + if (__builtin_expect(strbuf->length == 0, false)) { + return compLength == 0; // An empty list contains only the empty segment + } + uint32_t startIndex = 0; - while (startIndex < strbuf->length) { + while (startIndex <= strbuf->length) { // `<=` so a trailing empty segment (e.g. "abc:") is checked uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator); uint32_t folderLength = colonIndex - startIndex; @@ -819,8 +822,12 @@ bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, con } bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) { + if (__builtin_expect(strbuf->length == 0, false)) { + return compLength == 0; // An empty list contains only the empty segment + } + uint32_t startIndex = 0; - while (startIndex < strbuf->length) { + while (startIndex <= strbuf->length) { // `<=` so a trailing empty segment (e.g. "abc:") is checked uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator); uint32_t folderLength = colonIndex - startIndex; diff --git a/tests/strbuf.c b/tests/strbuf.c index d1ca64265..06cfe4754 100644 --- a/tests/strbuf.c +++ b/tests/strbuf.c @@ -681,6 +681,30 @@ int main(void) { VERIFY(ffStrbufMatchSeparatedS(&strbuf, ":abcdef", ':') == false); } + { + // ffStrbufMatchSeparatedNS with explicit compLength: the length-bounded + // contract is load-bearing for the Linux keyboard handler, which passes + // a fixed-size buffer length rather than a NUL-terminated string. + ffStrbufSetStatic(&strbuf, "abc"); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 11, "abc:def:ghi", ':') == true); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 3, "abc:def:ghi", ':') == true); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 7, "abc:def:ghi", ':') == true); // "abc:def" + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 4, "abc:def:ghi", ':') == true); // "abc:" + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 5, "abc:def:ghi", ':') == true); // "abc:d" truncated, but "abc" is a complete segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 3, "abd:def:ghi", ':') == false); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 0, "abc:def:ghi", ':') == false); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 0, "", ':') == false); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 11, "abc:def:ghi", ' ') == false); // no separator in bounds + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 12, "abc:def:ghi\0j", ':') == true); // embedded NUL inside bounds + + ffStrbufClear(&strbuf); + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 11, "abc:def:ghi", ':') == false); // no empty segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 8, "abc::def", ':') == true); // empty segment in the middle + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 4, "abc:", ':') == true); // trailing empty segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 4, ":abc", ':') == true); // leading empty segment + VERIFY(ffStrbufMatchSeparatedNS(&strbuf, 0, "abc", ':') == true); // empty separated string itself + } + { ffStrbufSetStatic(&strbuf, "ABC"); VERIFY(ffStrbufMatchSeparatedIgnCaseS(&strbuf, "abc:def:ghi", ' ') == false); @@ -721,6 +745,27 @@ int main(void) { VERIFY(ffStrbufSeparatedContainIgnCaseS(&strbuf, "i", ':') == false); } + { + // ffStrbufSeparatedContainNS with empty strings: an empty list contains + // only the empty segment (symmetric to ffStrbufMatchSeparatedNS); an + // empty comp matches only an empty segment in the list. + ffStrbufClear(&strbuf); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // empty list contains the empty segment + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 3, "abc", ':') == false); // empty list contains no non-empty segment + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "abc", ':') == true); // compLength == 0 means an empty segment + + ffStrbufSetStatic(&strbuf, "abc::def"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // empty segment in the middle + ffStrbufSetStatic(&strbuf, "abc:"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // trailing empty segment + ffStrbufSetStatic(&strbuf, ":abc"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == true); // leading empty segment + ffStrbufSetStatic(&strbuf, "abc:def"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == false); // no empty segment + ffStrbufSetStatic(&strbuf, "abc"); + VERIFY(ffStrbufSeparatedContainNS(&strbuf, 0, "", ':') == false); // no empty segment + } + { ffStrbufSetStatic(&strbuf, "abc"); ffStrbufSubstr(&strbuf, 0, 1); // start, end