Skip to content

mruby-regexp: fold a byte-indexed subject by ASCII under a /i backreference - #7264

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-binary-backref-fold
Aug 18, 2026
Merged

mruby-regexp: fold a byte-indexed subject by ASCII under a /i backreference#7264
matz merged 1 commit into
mruby:masterfrom
takumin:regexp-binary-backref-fold

Conversation

@takumin

@takumin takumin commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

A backreference under /i folded a byte-indexed subject (String#b) as if each byte were the codepoint of the same value. On a build with the Unicode case table 0xC0 folded to 0xE0 the way U+00C0 folds to U+00E0, so two bytes that spell no letter in common compared equal:

"\xC0\xE0".b =~ /(.)\1/i      # CRuby: nil, mruby before: 0, mruby after: nil
"\xC0a\xE0A".b =~ /(..)\1/i   # CRuby: nil, mruby before: 0, mruby after: nil
"\xC0\xC0".b =~ /(.)\1/i      # CRuby: 0,   mruby before: 0, mruby after: 0
"aA".b =~ /(.)\1/i            # CRuby: 0,   mruby before: 0, mruby after: 0
"sſ" =~ /(.)\1/i              # CRuby: 0,   mruby before: 0, mruby after: 0
"ſs".b =~ /(.)\1/i            # CRuby: nil, mruby before: nil, mruby after: nil

memcmp_ci in re_exec.c compares the captured span and the text at the cursor by folding both sides with mrb_re_case_fold. mrb_re_decode_char hands out the raw byte for a byte-indexed subject, and the fold took that byte for a codepoint. The class side of the same report ("\xE5".b =~ /[\xC5]/i) was already answered by class_match, which keeps a byte member apart from the codepoint of the same number; the backreference was the one path that still folded a subject at match time.

Fix

memcmp_ci folds through a new subject_fold(c, binary), which returns a byte above 127 unchanged when the subject is byte-indexed and folds everything else as before. The letters a byte can spell are the ASCII ones, and those still fold; a subject read as characters is untouched. Builds without the Unicode table were never affected (their fold leaves 0xC0 alone), so the fix changes nothing there.

Testing

New test Regexp - a backreference under /i folds a byte-indexed subject by ASCII in mrbgems/mruby-regexp/test/regexp_utf8.rb. The byte-indexed lines hold on every build and run unconditionally; the contrast with a subject read as characters ("sſ" =~ /(.)\1/i folds U+017F to s, "sſ".b does not) sits in an if __ENCODING__ == "UTF-8" branch rather than behind a skip, so a failure in the lines before it is not swallowed on the byte build. The test fails on master (Expected 0 to be nil) and passes with the fix.

Full suite green at every commit (one commit).

Build (MRUBY_CONFIG=ci/gcc-clang) Total KO Crash
full-debug 2351 0 0
bintest 2351 0 0
cxx_abi 2351 0 0
byte-string 2281 0 0
ascii-case 2348 0 0
default (rake -m test, no MRUBY_CONFIG) 2127 0 0

One divergence from CRuby is left alone, since it is not about byte-indexed subjects: CRuby answers "ſs" =~ /(.)\1/i with nil while "sſ" =~ /(.)\1/i is 0, because Onigmo requires the remaining subject to hold at least the captured span's byte length before it folds. mruby answers 0 for both orders.

Environment

Machine, toolchain, and the compile line of every build
Item Value
OS Ubuntu 24.04.4 LTS
Kernel 7.0.0-29-generic
CPU AMD Ryzen 9 5950X 16-Core Processor
C compiler gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
binutils GNU ld (GNU Binutils) 2.47.20260726
rake rake, version 13.3.1
CRuby (reference) ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]

Actual compile line of src/string.c in each build_config/ci/gcc-clang.rb build (-MMD -c, -I, and -o dropped). full-debug is -O0 because enable_debug appends -g3 -O0 after the toolchain's -g -O3; cxx_abi compiles C as C++ with gcc -x c++ -std=gnu++03, g++ only links.

# full-debug
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -g3 -O0 -DMRB_GC_STRESS -DMRB_USE_DEBUG_HOOK -DMRB_DEBUG -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c
# bintest
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_GC_FIXED_ARENA -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -DMRB_USE_DEBUG_HOOK src/string.c
# cxx_abi
gcc -g -O3 -Wall -Wundef -Wwrite-strings -x c++ -std=gnu++03 -DMRB_GC_FIXED_ARENA -DMRB_USE_CXX_EXCEPTION -DMRB_USE_CXX_ABI -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c
# byte-string
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c
# ascii-case
gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_ASCII_CASE -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER src/string.c

Summary by CodeRabbit

  • Bug Fixes

    • Improved case-insensitive regular expression matching for binary data.
    • ASCII byte values are folded correctly without incorrectly applying Unicode mappings to non-ASCII bytes.
    • Case-insensitive backreferences now behave consistently for byte-indexed subjects and valid UTF-8 text.
  • Tests

    • Added regression coverage for binary and UTF-8 case-insensitive matching scenarios.

…eference

A backreference under `/i` compares the captured span and the text at the
cursor through `memcmp_ci`, which folds both sides with `mrb_re_case_fold`.
For a byte-indexed subject (`String#b`) `mrb_re_decode_char` hands out the
raw byte, and on a build with the Unicode table that byte was folded as the
codepoint of the same value: 0xC0 became 0xE0 the way U+00C0 folds to
U+00E0, so two bytes that spell no letter in common compared equal.

```ruby
"\xC0\xE0".b =~ /(.)\1/i      # CRuby: nil, mruby: 0
"\xC0a\xE0A".b =~ /(..)\1/i   # CRuby: nil, mruby: 0
```

A byte is not the character of the same value, and the letters a byte can
spell are the ASCII ones. `memcmp_ci` now folds through `subject_fold`, which
returns a byte above 127 unchanged when the subject is byte-indexed and folds
everything else as before, so `"aA".b` and `"\xC0\xC0".b` still match
`/(.)\1/i` at 0 and the two bytes above do not. A subject read as characters
is untouched: `"sſ" =~ /(.)\1/i` still folds U+017F to `s`.

The class side of the same report (`"\xE5".b =~ /[\xC5]/i`) was already
answered: `class_match` keeps a byte member apart from the codepoint of the
same number, so a byte-indexed subject reaches only the byte half of a class.
The backreference was the one path that folded a subject at match time.
@takumin
takumin requested a review from matz as a code owner August 18, 2026 10:09
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e5dfbfd-bb2a-4f29-869d-7a627c5fa180

📥 Commits

Reviewing files that changed from the base of the PR and between 885e215 and a10097c.

📒 Files selected for processing (2)
  • mrbgems/mruby-regexp/src/re_exec.c
  • mrbgems/mruby-regexp/test/regexp_utf8.rb

Included review availability: Your plan includes up to 8 reviews per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Changes

Binary Subject Case Folding

Layer / File(s) Summary
Subject-aware folding and regression coverage
mrbgems/mruby-regexp/src/re_exec.c, mrbgems/mruby-regexp/test/regexp_utf8.rb
Binary subjects preserve non-ASCII bytes during case-insensitive matching. ASCII folding remains active, Unicode folding remains active for valid UTF-8 characters, and regression tests cover these cases.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to a1009

This localized change prevents incorrect case-insensitive backreference matches for byte-indexed strings while preserving existing ASCII and character-string behavior. No actionable merge-blocking risk remains after normal checks and review.

Possibly related PRs

  • mruby/mruby#7046: Directly extends /i backreference comparison in re_exec.c.
  • mruby/mruby#7183: Reworks Unicode case-folding infrastructure used by regexp matching.
  • mruby/mruby#7190: Modifies mruby-regexp case-insensitive folding behavior.

Suggested labels: mrbgems

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: ASCII-only folding for byte-indexed subjects in case-insensitive backreferences.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@matz
matz merged commit 2931540 into mruby:master Aug 18, 2026
21 checks passed
@takumin
takumin deleted the regexp-binary-backref-fold branch August 18, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants