From bea6410138c538908bb15639675e9835c66e70bc Mon Sep 17 00:00:00 2001 From: Volodymyr Chernetskyi Date: Tue, 8 Sep 2026 15:50:03 +0200 Subject: [PATCH] vim-patch:9.2.1046: regex: case-insensitive match fails on multi-byte string with re=1 (#41781) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: With 'regexpengine' set to 1 a case-insensitive match against a literal string fails when the string starts with a multi-byte character that is longer than a character following it, so the two regexp engines disagree (after v9.1.0645). Solution: In cstrncmp() advance by the length of the character at the current position instead of always measuring the first character of "s1" (Volodymyr Chernetskyi). cstrncmp() walks "s1" to find how many characters make up "*n" bytes, so that it can measure out the same number of characters in "s2". The loop decremented the remaining byte count by mb_ptr2len(s1), which always returns the length of the *first* character, rather than the length of the character at the current position "p". When the first character is longer than a later one the byte count runs out too early, the character count comes up short, and MB_STRNICMP2() is handed a length for "s2" that is too small, so the comparison fails. For example matching "\cüber" against "Überraschung": "über" is five bytes, but each iteration subtracts two (the length of "ü"), so the loop runs three times instead of four. :set regexpengine=1 echo matchstr('Überraschung', '\cüber') returns an empty string, while 'regexpengine' set to 2 correctly returns "Über". The default value of 0 uses the NFA engine and is unaffected. related: vim/vim#14756 closes: vim/vim#21212 https://github.com/vim/vim/commit/4e5ac0d68a3e35602b63fba7b334baa9dc7cc22e --- src/nvim/regexp.c | 2 +- test/old/testdir/test_regexp_utf8.vim | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index 3b63c6ec8f..5074e3a8d5 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -1759,7 +1759,7 @@ static int cstrncmp(char *s1, char *s2, int *n) int n1 = *n; // count the number of characters for byte-length of s1 while (n1 > 0 && *p != NUL) { - n1 -= utfc_ptr2len(s1); + n1 -= utfc_ptr2len(p); MB_PTR_ADV(p); n2++; } diff --git a/test/old/testdir/test_regexp_utf8.vim b/test/old/testdir/test_regexp_utf8.vim index f5ec0b77d2..272d3d535e 100644 --- a/test/old/testdir/test_regexp_utf8.vim +++ b/test/old/testdir/test_regexp_utf8.vim @@ -363,6 +363,12 @@ func Run_regexp_ignore_case() call assert_equal('iIx', substitute('iIİ', '\c\(\%u0130\)', 'x', 'g')) call assert_equal('iIx', substitute('iIİ', '\c\([\u0130]\)', 'x', 'g')) call assert_equal('iIx', substitute('iIİ', '\c\([\u012f-\u0131]\)', 'x', 'g')) + + " Ignoring case in a literal string that starts with a character longer + " than a following one must still match. + call assert_equal('Über', matchstr('Überraschung', '\cüber')) + call assert_equal('Ünder', matchstr('Ünderdog', '\cünder')) + call assert_equal('αaaa', matchstr('αaaaa', '\cαaaa')) endfunc func Test_regexp_ignore_case()