Thanks for the bug report. I installed the attached, which is a bit fancier 
than what you suggested in order to handle the tricky cases that  
case_folded_counterparts handles, e.g., Greek where a single uppercase 
character (Σ) can have multiple lowercase counterparts (σ, ς, ϲ). The first 
patch fixes the bug and adds a test case; the second updates commentary.

I have the feeling that there are other bugs in this area, with respect to 
unusual Unicode characters. I'll try to find time to fix that.
From cb6cb332a99afe0f28bc0c844e8f0fd12614d0ac Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Sun, 11 Oct 2026 12:16:25 -0700
Subject: [PATCH 1/2] regex: fix REG_ICASE for single-byte chars

Problem reported by Arno Onken in:
https://lists.gnu.org/r/bug-gnulib/2026-10/msg00029.html
* lib/regcomp.c (re_compile_fastmap_iter): For SIMPLE_BRACKET,
also set fastmap for leading bytes of case folded counterparts
of each character.
* tests/test-regex-el.c (main): Test [C] as well as C,
for each problematic character C.
---
 ChangeLog             | 11 ++++++
 lib/regcomp.c         | 12 +++++--
 tests/test-regex-el.c | 84 +++++++++++++++++++++++--------------------
 3 files changed, 65 insertions(+), 42 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index d3b4411658..c0d6c0dbb4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,14 @@
+2026-10-11  Paul Eggert  <[email protected]>
+
+	regex: fix REG_ICASE for single-byte chars
+	Problem reported by Arno Onken in:
+	https://lists.gnu.org/r/bug-gnulib/2026-10/msg00029.html
+	* lib/regcomp.c (re_compile_fastmap_iter): For SIMPLE_BRACKET,
+	also set fastmap for leading bytes of case folded counterparts
+	of each character.
+	* tests/test-regex-el.c (main): Test [C] as well as C,
+	for each problematic character C.
+
 2026-10-11  Bruno Haible  <[email protected]>
 
 	Make C++ templates more hygienic.
diff --git a/lib/regcomp.c b/lib/regcomp.c
index ab3783f94c..89e16bcaef 100644
--- a/lib/regcomp.c
+++ b/lib/regcomp.c
@@ -338,14 +338,20 @@ re_compile_fastmap_iter (regex_t *bufp, const re_dfastate_t *init_state,
 	}
       else if (type == SIMPLE_BRACKET)
 	{
-	  int i, ch;
-	  for (i = 0, ch = 0; i < BITSET_WORDS; ++i)
+	  unsigned char ch = 0;
+	  for (int i = 0; i < BITSET_WORDS; i++)
 	    {
 	      int j;
 	      bitset_word_t w = dfa->nodes[node].opr.sbcset[i];
 	      for (j = 0; j < BITSET_WORD_BITS; ++j, ++ch)
 		if (w & ((bitset_word_t) 1 << j))
-		  re_set_fastmap (fastmap, ch);
+		  {
+		    re_set_fastmap (fastmap, ch);
+		    wchar_t wc;
+		    mbstate_t state = {0};
+		    if (__mbrtowc (&wc, (char *) &ch, 1, &state) == 1)
+		      re_set_fastmap_icase (fastmap, wc, &state);
+		  }
 	    }
 	}
       else if (type == COMPLEX_BRACKET)
diff --git a/tests/test-regex-el.c b/tests/test-regex-el.c
index 7cdf579d70..1e2589a3b2 100644
--- a/tests/test-regex-el.c
+++ b/tests/test-regex-el.c
@@ -74,45 +74,51 @@ main ()
 
   for (int i = 0; i < 3; i++)
     for (int j = 0; j < 3; j++)
-      {
-        static char const str[3][2] = { "\xd3", "\xf2", "\xf3" };
-        re_set_syntax (RE_ICASE);
-        memset (&regex, 0, sizeof regex);
-        s = re_compile_pattern (str[i], 1, &regex);
-        if (s)
-          {
-            report_error ("re_compile_pattern \\x%02x failed: %s",
-                          (unsigned char) str[i][0], s);
-            continue;
-          }
-        int without = re_search (&regex, str[j], 1, 0, 1, NULL);
-        regfree (&regex);
-
-        memset (&regex, 0, sizeof regex);
-        regex.fastmap = malloc (UCHAR_MAX + 1);
-        if (!regex.fastmap)
-          {
-            report_error ("malloc failed");
-            continue;
-          }
-        s = re_compile_pattern (str[i], 1, &regex);
-        if (s)
-          {
-            report_error ("re_compile_pattern \\x%02x failed(!): %s",
-                          (unsigned char) str[i][0], s);
-            continue;
-          }
-        int with = re_search (&regex, str[j], 1, 0, 1, NULL);
-
-        if (with != without)
-          report_error
-            ("fastmap mismatch: pattern = \\x%02x, string = \\x%02x,"
-             " with = %d, without = %d",
-             (unsigned char) str[i][0], (unsigned char) str[j][0],
-             with, without);
-
-        regfree (&regex);
-      }
+      for (int k = 0; k < 2; k++)
+        {
+          static char const str[3][2] = { "\xd3", "\xf2", "\xf3" };
+          char pat[4];
+          char const *lbrack = &"["[k];
+          char const *rbrack = &"]"[k];
+          int patlen = sprintf (pat, "%s%s%s", lbrack, str[i], rbrack);
+          re_set_syntax (RE_ICASE);
+          memset (&regex, 0, sizeof regex);
+          s = re_compile_pattern (pat, patlen, &regex);
+          if (s)
+            {
+              report_error ("re_compile_pattern %s\\x%02x%s failed: %s",
+                            lbrack, (unsigned char) str[i][0], rbrack, s);
+              continue;
+            }
+          int without = re_search (&regex, str[j], 1, 0, 1, NULL);
+          regfree (&regex);
+
+          memset (&regex, 0, sizeof regex);
+          regex.fastmap = malloc (UCHAR_MAX + 1);
+          if (!regex.fastmap)
+            {
+              report_error ("malloc failed");
+              continue;
+            }
+          s = re_compile_pattern (pat, patlen, &regex);
+          if (s)
+            {
+              report_error ("re_compile_pattern %s\\x%02x%s failed(!): %s",
+                            lbrack, (unsigned char) str[i][0], rbrack, s);
+              continue;
+            }
+          int with = re_search (&regex, str[j], 1, 0, 1, NULL);
+
+          if (with != without)
+            report_error
+              ("fastmap mismatch: pattern = %s\\x%02x%s, string = \\x%02x,"
+               " with = %d, without = %d",
+               lbrack, (unsigned char) str[i][0], rbrack,
+               (unsigned char) str[j][0],
+               with, without);
+
+          regfree (&regex);
+        }
 
   return exit_status;
 }
-- 
2.53.0

From 73734354ea0f4b60329c39e316fc772be1e97a16 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email protected]>
Date: Sun, 11 Oct 2026 16:19:34 -0700
Subject: [PATCH 2/2] localeinfo: improve comments

* lib/localeinfo.c (lonesome_lower): In comments, show how to
derive this table, and describe better what the table is for.
---
 ChangeLog        |  4 +++
 lib/localeinfo.c | 69 +++++++++++++++++++++++++++++++++++++++++-------
 2 files changed, 63 insertions(+), 10 deletions(-)

diff --git a/ChangeLog b/ChangeLog
index c0d6c0dbb4..be2fcbefc5 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,9 @@
 2026-10-11  Paul Eggert  <[email protected]>
 
+	localeinfo: improve comments
+	* lib/localeinfo.c (lonesome_lower): In comments, show how to
+	derive this table, and describe better what the table is for.
+
 	regex: fix REG_ICASE for single-byte chars
 	Problem reported by Arno Onken in:
 	https://lists.gnu.org/r/bug-gnulib/2026-10/msg00029.html
diff --git a/lib/localeinfo.c b/lib/localeinfo.c
index 864eb1f5c4..066243bf48 100644
--- a/lib/localeinfo.c
+++ b/lib/localeinfo.c
@@ -105,23 +105,72 @@ init_localeinfo (struct localeinfo *localeinfo)
 }
 
 /* The set of char32_t values C such that there's a useful locale
-   somewhere where C != c32toupper (C) && C != c32tolower (c32toupper (C)).
+   where C is not reached from D by calling c32isupper and c32islower
+   repeatedly, even though C and D should match when ignoring case.
    For example, 0x00B5 (U+00B5 MICRO SIGN) is in this table, because
-   c32toupper (0x00B5) == 0x039C (U+039C GREEK CAPITAL LETTER MU), and
-   c32tolower (0x039C) == 0x03BC (U+03BC GREEK SMALL LETTER MU).  */
+   in a typical Unicode locale c32toupper and c32tolower never return 0x00B5
+   even though when ignoring case 0x00B5 should match both
+   0x039C (U+039C GREEK CAPITAL LETTER MU) and
+   0x03BC (U+03BC GREEK SMALL LETTER MU) because
+   c32toupper (0x00B5) == 0x039C and c32tolower (0x039C) == 0x03BC.  */
 static unsigned short int const lonesome_lower[] =
   {
-    0x00B5, 0x0131, 0x017F, 0x01C5, 0x01C8, 0x01CB, 0x01F2, 0x0345,
-    0x03C2, 0x03D0, 0x03D1, 0x03D5, 0x03D6, 0x03F0, 0x03F1,
-
 #if !defined __STDC_ISO_10646__ || __STDC_ISO_10646__ < 200305L
-    /* U+03F2 GREEK LUNATE SIGMA SYMBOL lacks a specific uppercase
+    /* This lacks a specific uppercase
        counterpart in locales predating Unicode 4.0.0 (April 2003).  */
-    0x03F2,
+    0x03F2, /* GREEK LUNATE SIGMA SYMBOL */
 #endif
 
-    0x03F5, 0x1C80, 0x1C81, 0x1C82, 0x1C83, 0x1C84, 0x1C85, 0x1C86,
-    0x1C87, 0x1C88, 0x1E9B, 0x1FBE,
+    /* Run the following shell command with the latest UnicodeData.txt:
+
+         awk -F';' '
+           {
+             c = $1 ""
+             name[c] = $2 ""
+             upcase[c] = $13 != "" ? $13 "" : c
+             downcase[c] = $14 != "" ? $14 "" : c
+             codes[ncodes++] = c
+           }
+           END {
+             for (i = 0; i < ncodes; i++) {
+               c = codes[i]
+               upper_c = upcase[c]
+               lower_upper_c = downcase[upper_c]
+               if (c != upper_c && c != lower_upper_c)
+                 printf "    0x%s, /""* %s *""/\n", c, name[c]
+             }
+           }
+         ' UnicodeData.txt
+
+       to generate the following list:  */
+
+    0x00B5, /* MICRO SIGN */
+    0x0131, /* LATIN SMALL LETTER DOTLESS I */
+    0x017F, /* LATIN SMALL LETTER LONG S */
+    0x01C5, /* LATIN CAPITAL LETTER D WITH SMALL LETTER Z WITH CARON */
+    0x01C8, /* LATIN CAPITAL LETTER L WITH SMALL LETTER J */
+    0x01CB, /* LATIN CAPITAL LETTER N WITH SMALL LETTER J */
+    0x01F2, /* LATIN CAPITAL LETTER D WITH SMALL LETTER Z */
+    0x0345, /* COMBINING GREEK YPOGEGRAMMENI */
+    0x03C2, /* GREEK SMALL LETTER FINAL SIGMA */
+    0x03D0, /* GREEK BETA SYMBOL */
+    0x03D1, /* GREEK THETA SYMBOL */
+    0x03D5, /* GREEK PHI SYMBOL */
+    0x03D6, /* GREEK PI SYMBOL */
+    0x03F0, /* GREEK KAPPA SYMBOL */
+    0x03F1, /* GREEK RHO SYMBOL */
+    0x03F5, /* GREEK LUNATE EPSILON SYMBOL */
+    0x1C80, /* CYRILLIC SMALL LETTER ROUNDED VE */
+    0x1C81, /* CYRILLIC SMALL LETTER LONG-LEGGED DE */
+    0x1C82, /* CYRILLIC SMALL LETTER NARROW O */
+    0x1C83, /* CYRILLIC SMALL LETTER WIDE ES */
+    0x1C84, /* CYRILLIC SMALL LETTER TALL TE */
+    0x1C85, /* CYRILLIC SMALL LETTER THREE-LEGGED TE */
+    0x1C86, /* CYRILLIC SMALL LETTER TALL HARD SIGN */
+    0x1C87, /* CYRILLIC SMALL LETTER TALL YAT */
+    0x1C88, /* CYRILLIC SMALL LETTER UNBLENDED UK */
+    0x1E9B, /* LATIN SMALL LETTER LONG S WITH DOT ABOVE */
+    0x1FBE, /* GREEK PROSGEGRAMMENI */
   };
 
 /* Verify that the worst multibyte case fits.  This is 1 for c32toupper, 1 for
-- 
2.53.0

Reply via email to