mruby-string-ext: validate UTF-8 sequences with mrb_utf8len - #7105
Merged
Conversation
Core stopped accepting the UTF-8 sequences RFC 3629 forbids in mruby#7093, and `utf8code()` in this gem was taught the same rules by hand. The tests for those rules go through `String#scrub`, which reads its lengths from a different helper, so nothing holds `ord` and `codepoints` to them. Pin them: a stray continuation byte, a truncated sequence, an overlong encoding, a surrogate, a code point above U+10FFFF and a five-byte lead all raise. `String#codepoints` also has to keep its place across a 4-byte character, so a string that continues past one is checked too.
`str_scrub_char_len()` asks whether the bytes at p start a valid UTF-8 character and, if they do, how many bytes it takes. `mrb_utf8len()` answers that question in core, and answers it by the same rules: both were brought to RFC 3629 in mruby#7093, one after the other. What has to be bridged is how a refusal comes back. `mrb_utf8len()` answers 1 for every sequence it turns down, and a real one-byte character is a lead byte below 0x80, so a length of 1 on a byte at or above 0x80 is the refusal. The two were checked against each other over both leading bytes, nine representative tail bytes and every available length from 1 to 4: 21,233,664 cases, no difference.
`utf8code()` judged the sequence itself before decoding it: the continuation bytes, then the RFC 3629 rules for overlong encodings, surrogates and code points above U+10FFFF, all written a second time. `mrb_utf8len()` passes that same judgment, so ask it and decode what it accepts. `String#codepoints` stepped over each character by the raw `mrb_utf8len_table`, which is the length a lead byte claims rather than the length that was accepted. The two agree wherever `utf8code()` returns, but the caller no longer has to know that: the byte length comes back through `lenp`. The table is still read by `String#chars`, so the declaration stays. The two were checked against each other over the same 21,233,664 cases as `str_scrub_char_len()`, with no difference in the code point, in what is refused, or in the byte length.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughUTF-8 decoding now uses shared sequence-length validation. ChangesUTF-8 validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
This was referenced Aug 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mrb_utf8len()decides what counts as a UTF-8 character in core, under theRFC 3629 rules it was given in #7093. mruby-string-ext decides it twice more,
in
utf8code()and instr_scrub_char_len(), and both were taught those samerules by hand in that same PR. Ask core instead.
Reading a refusal
mrb_utf8len()answers 1 for every sequence it turns down. A real one-bytecharacter is a lead byte below 0x80, so a length of 1 on a byte at or above
0x80 is the refusal. That is the whole bridge; both helpers are a few lines on
top of it.
utf8code()keeps only the part core has no answer for, the code point itself,and reports the byte length it decoded through a new
lenpargument.String#codepointswas stepping over characters by the rawmrb_utf8len_table, which is the length a lead byte claims rather than thelength that was accepted; it now steps by what came back. The table is still
read by
String#chars, so the declaration stays.22 lines of C replace 50.
Equivalence
Nothing here is meant to change behavior, so the old and new helpers were run
against each other over both leading bytes, nine representative tail bytes and
every available length from 1 to 4: 21,233,664 cases, with no difference in
the code point, in what is refused, or in the byte length.
Tests
String#ordandString#codepointshad nothing holding them to the RFC 3629rules; the tests for those go through
String#scrub, which reads its lengthsfrom the other helper. The first commit pins them, and it passes both before
and after the two that follow.
Verified
rake teston a full-core build (MRB_UTF8_STRINGthrough mruby-encoding):2243 tests, all green, at each of the three commits
rake teston the default gembox (noMRB_UTF8_STRING): 2055 tests, allgreen
prek run --all-filespasses, except thatmarkdownlintcould not installlocally (npm engine mismatch); no Markdown is touched here
Summary by CodeRabbit
Bug Fixes
String#ordandString#codepointsnow correctly reject malformed UTF-8 sequences withArgumentError.Tests