Commit: patch 9.2.0906: slow transstr() with long strings

Christian Brabandt <[email protected]> Mon, 3 Aug 2026 22:45:04 +0200
Newsgroups gmane.editors.vim.devel
Message-ID <[email protected]>
patch 9.2.0906: slow transstr() with long strings

Commit: https://github.com/vim/vim/commit/124c86868c253a5ec1347e7cbe102504d=
2d66a07
Author: Samuel Schlesinger <[email protected]>
Date:   Mon Aug 3 20:30:09 2026 +0000

    patch 9.2.0906: slow transstr() with long strings
   =20
    Problem:  transstr() appends with STRCAT()/STRLEN() from the start of
              the result on every iteration, making it quadratic to the
              length of the string.
    Solution: Keep a tail pointer and append at it. (Samuel Schlesinger).
   =20
    closes: #20925
   =20
    Signed-off-by: Samuel Schlesinger <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/src/charset.c b/src/charset.c
index 1c45688fe..3be32f5fb 100644
--- a/src/charset.c
+++ b/src/charset.c
@@ -383,7 +383,10 @@ transstr(char_u *s)
     if (res =3D=3D NULL)
 	return NULL;
=20
-    *res =3D NUL;
+    // Keep a tail pointer to append to, appending with STRCAT would make
+    // this loop quadratic.
+    char_u *d =3D res;
+
     p =3D s;
     while (*p !=3D NUL)
     {
@@ -391,14 +394,28 @@ transstr(char_u *s)
 	{
 	    c =3D (*mb_ptr2char)(p);
 	    if (vim_isprintc(c))
-		STRNCAT(res, p, l);	// append printable multi-byte char
+	    {
+		// append printable multi-byte char
+		mch_memmove(d, p, (size_t)l);
+		d +=3D l;
+	    }
 	    else
-		transchar_hex(res + STRLEN(res), c);
+	    {
+		transchar_hex(d, c);
+		d +=3D STRLEN(d);
+	    }
 	    p +=3D l;
 	}
 	else
-	    STRCAT(res, transchar_byte(*p++));
+	{
+	    char_u	*trs =3D transchar_byte(*p++);
+	    int		trs_len =3D (int)STRLEN(trs);
+
+	    mch_memmove(d, trs, (size_t)trs_len);
+	    d +=3D trs_len;
+	}
     }
+    *d =3D NUL;
     return res;
 }
=20
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vi=
m
index 645ff531e..2f2a7fc5f 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -211,6 +211,56 @@ func Test_strwidth()
   set ambiwidth&
 endfunc
=20
+func Test_strtrans()
+  " The default of 'isprint' is platform-dependent: 0x7f and 0x9f are
+  " printable on Win32 and VMS.  Set it so the expectations below hold
+  " everywhere.
+  let save_isprint =3D &isprint
+  set isprint=3D@,161-255
+
+  " printable ASCII is unchanged
+  call assert_equal('', strtrans(''))
+  call assert_equal('abc', strtrans('abc'))
+
+  " control characters are displayed as ^X
+  call assert_equal('^I', strtrans("	"))
+  call assert_equal('a^Mb^[c', strtrans("a
b c"))
+  call assert_equal('^A^_^?', strtrans("\x01\x1f\x7f"))
+
+  " printable multibyte characters are unchanged, including composing
+  " characters and characters above 0xffff
+  call assert_equal('h=C3=A9llo =E4=BD=A0=E5=A5=BD', strtrans('h=C3=A9llo =
=E4=BD=A0=E5=A5=BD'))
+  let s =3D 'e' .. nr2char(0x301) .. 'x'
+  call assert_equal(s, strtrans(s))
+  call assert_equal(nr2char(0x1d11e), strtrans(nr2char(0x1d11e)))
+
+  " unprintable multibyte characters are displayed in <xx> hex form
+  call assert_equal('<9f>', strtrans(nr2char(0x9f)))
+  call assert_equal('<200b>', strtrans(nr2char(0x200b)))
+  call assert_equal('<feff>', strtrans(nr2char(0xfeff)))
+
+  " illegal bytes are displayed in <xx> hex form
+  call assert_equal('A<ff>B', strtrans("A\xffB"))
+
+  " a long string mixing all kinds of characters
+  call assert_equal(repeat('a^B=C3=A9<9f>', 100),
+        \ strtrans(repeat("a\x02=C3=A9" .. nr2char(0x9f), 100)))
+
+  " the non-multi-byte code path
+  set encoding=3Dlatin1
+  set isprint=3D@,161-255
+  call assert_equal('a^Mb^[c', strtrans("a
b c"))
+  call assert_equal('^A^_^?', strtrans("\x01\x1f\x7f"))
+  " an unprintable byte above 0x7f uses the meta notation
+  call assert_equal('| ', strtrans("\xa0"))
+  " a printable high byte is unchanged
+  call assert_equal("\xe9", strtrans("\xe9"))
+  call assert_equal("x^B\xe9| y", strtrans("x\x02\xe9\xa0y"))
+  set encoding=3Dutf-8
+
+  let &isprint =3D save_isprint
+endfunc
+
 func Test_str2nr()
   call assert_equal(0, str2nr(''))
   call assert_equal(1, str2nr('1'))
diff --git a/src/version.c b/src/version.c
index 7bb915f93..13c9fc7d3 100644
--- a/src/version.c
+++ b/src/version.c
@@ -763,6 +763,8 @@ static char *(features[]) =3D
=20
 static int included_patches[] =3D
 {   /* Add new patch number below this line */
+/**/
+    906,
 /**/
     905,
 /**/

--=20
--=20
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

---=20
You received this message because you are subscribed to the Google Groups "=
vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an e=
mail to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/vim_dev/E1w=
qzWy-00BQyS-EI%40256bit.org.