Skip to content

Move the String#valid_encoding? body into core as mrb_str_valid_encoding_p - #7102

Merged
matz merged 2 commits into
mruby:masterfrom
takumin:str-valid-encoding-p
Aug 12, 2026
Merged

Move the String#valid_encoding? body into core as mrb_str_valid_encoding_p#7102
matz merged 2 commits into
mruby:masterfrom
takumin:str-valid-encoding-p

Conversation

@takumin

@takumin takumin commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

String#valid_encoding? in mruby-encoding walks the string with
mrb_utf8len and counts the characters. mrb_utf8_strlen() in string.c,
reached through utf8_strlen(), walks a string for the same purpose. The
two loops make the same decisions about how a UTF-8 string is read:

  • the byte length of a character comes from mrb_utf8len
  • the walk stops at the end of the range it was given, rather than at
    whatever bytes follow it
  • a binary string is answered without walking it at all
  • a string that turns out to hold as many characters as it has bytes is
    marked MRB_STR_SINGLE_BYTE on the way out

Only two steps are not shared: the gem tests each character for a byte that
stands for no character, and core skips runs of ASCII a word at a time.

This moves the body into string.c as mrb_str_valid_encoding_p and
leaves the gem method as a wrapper. The one walk that remains does both:
it skips the ASCII a word at a time, and it reports the first byte that
stands for no character. Neither step can hide the other, since a skipped
byte is ASCII and therefore valid, and a continuation byte is never ASCII.

Why this belongs in core

The method stays in the gem. String#valid_encoding? is defined by
mruby-encoding exactly as before, and a build without the gem gains no
method. What moves is the walk that core already performs for String#size
and character indexing, and the single byte flag that utf8_strlen() sets
while doing it.

string.c

The body of mrb_utf8_strlen becomes a static utf8_strlen_check(str, byte_len, validp). With validp given, the walk stops at the first
non-ASCII byte that mrb_utf8len measures as one byte and sets *validp
to FALSE; the returned count is a character count only while *validp
stays TRUE. mrb_utf8_strlen calls it with NULL, so nothing changes for
its existing callers.

mrb_str_valid_encoding_p(mrb, str) sits on top of that worker with the
rules the gem has: a binary string is valid without a walk, and a string
whose character count equals its byte count gets the single byte flag. It
does not read that flag on the way in, keeping what 470396b established:
the flag says one byte per character, which a string of stray bytes
satisfies as well as an ASCII one.

internal.h

The declaration sits with mrb_str_char_to_byte and mrb_str_byte_to_char,
outside the MRB_UTF8_STRING guard, so a caller does not need to guard the
call. Without UTF-8 support a string is a sequence of bytes with no encoding
to disagree with, and the function answers TRUE.

mruby-encoding

str_valid_enc_p is now one line, and the gem no longer reaches into
RSTR_LEN, RSTR_PTR and RSTR_SET_SINGLE_BYTE_FLAG to answer this
question itself.

Behavior

No change to what String#valid_encoding? answers, and no change to when
it sets the single byte flag. What changes is the cost of the walk, since
core skips runs of ASCII a word at a time through search_nonascii()
instead of calling mrb_utf8len on every byte. Measured with the default
gembox plus mruby-encoding, gcc -O3, 200 calls per case, best of five, both
binaries built with the same config on the same machine and run one after
the other:

case before after
10000 ASCII bytes and one multi-byte character 5.78 ms 0.07 ms
10000 ASCII bytes 5.74 ms 0.07 ms
2500 multi-byte characters, 7500 bytes 1.47 ms 1.45 ms
one multi-byte character in every ten, 12000 bytes 5.79 ms 1.14 ms

A string that is all multi-byte gains nothing, having no run of ASCII to
skip, and nothing measured got slower. String#size reaches the same walk
now, through utf8_strlen_check(str, byte_len, NULL), and measures the same
as before.

Two tests are added for shapes the existing cases do not reach: a broken
byte found after a run of ASCII, and a substring long enough to share its
parent's buffer, whose walk has to stop at its own length.

Testing

The gem is in no gembox, so a default rake test builds neither the gem
nor MRB_UTF8_STRING. Both sides were run from a clean build/:

  • MRUBY_CONFIG=host-debug rake test (full-core, so the gem is in and
    MRB_UTF8_STRING is on): 2237 assertions, 0 KO, 0 crash, plus 116
    bintest.
  • rake test with the default config: 2053 assertions, 0 KO, 0 crash, plus
    105 bintest. This is the build where mrb_str_valid_encoding_p answers
    TRUE without a walk.
  • MRUBY_CONFIG=minimal rake, to confirm the function compiles where UTF-8
    support is absent.

prek run --all-files passes, except that markdownlint could not install
locally; no Markdown is touched here.

Summary by CodeRabbit

  • New Features

    • Added string encoding validation support through mrb_str_valid_encoding_p.
    • String#valid_encoding? now reliably handles UTF-8, binary strings, invalid trailing bytes, truncated sequences, and shared substrings.
  • Bug Fixes

    • Improved detection of invalid UTF-8 sequences, including cases following long ASCII content.

`String#valid_encoding?` in mruby-encoding walks the string itself and
sets `MRB_STR_SINGLE_BYTE` directly. `mrb_utf8_strlen` walks a string the
same way one function away, and `utf8_strlen` sets the same flag under the
same condition. Of the two walks, only the gem's walk tests each character
for a byte that stands for no character, and only core's skips a run of
ASCII a word at a time.

Rename the body of `mrb_utf8_strlen` to a static `utf8_strlen_check`
that takes an optional `mrb_bool *validp`. When it is given, the walk
stops at the first non-ASCII byte that `mrb_utf8len` measures as one
byte, which is a byte standing for no character, and reports it.
`mrb_utf8_strlen` passes `NULL` and is unchanged for its callers, in what
it answers and in what it costs.

`mrb_str_valid_encoding_p` builds on that worker and keeps what the gem
does: a binary string is valid without a walk, a string carrying a byte
that stands for no character is not, and a string that walks to as many
characters as it has bytes gets the single byte flag on the way out. The
flag is not read on the way in, for the reason 470396b gives: it says one
byte per character, which a string of stray bytes satisfies too.

Declare it outside the `MRB_UTF8_STRING` guard in internal.h, as
`mrb_str_char_to_byte` and `mrb_str_byte_to_char` are. A build without
UTF-8 support reads a string as bytes with no encoding to disagree with,
so it answers TRUE there.
`str_valid_enc_p` is now a wrapper over `mrb_str_valid_encoding_p`, and
the gem no longer reaches into `RSTR_LEN`, `RSTR_PTR` and
`RSTR_SET_SINGLE_BYTE_FLAG` to answer the question itself.

The answer and the single byte flag it leaves behind are unchanged. The
walk is now the one core runs for `mrb_utf8_strlen`, which skips a run of
ASCII a word at a time rather than calling `mrb_utf8len` on every byte; a
string of 10000 ASCII bytes is answered about 80 times faster, and one
that is all multi-byte measures the same as before.

Add tests for two shapes the existing cases do not reach. Those cases are
short enough to be decoded from the first byte without crossing a run of
ASCII, and none of them measures a substring that shares its parent's
buffer, where the walk has to stop at a length of its own rather than at
the parent's bytes.
@coderabbitai

coderabbitai Bot commented Aug 12, 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: 2113010e-33e0-4df5-887d-58c58a5617b8

📥 Commits

Reviewing files that changed from the base of the PR and between e24e810 and 00a12db.

📒 Files selected for processing (4)
  • include/mruby/internal.h
  • mrbgems/mruby-encoding/src/encoding.c
  • mrbgems/mruby-encoding/test/string.rb
  • src/string.c

📝 Walkthrough

Walkthrough

The PR adds mrb_str_valid_encoding_p, centralizes UTF-8 validation, updates the encoding gem to use it, and adds tests for invalid sequences, binary strings, and shared substring boundaries.

Changes

String encoding validation

Layer / File(s) Summary
Shared encoding validation
include/mruby/internal.h, src/string.c
Adds mrb_str_valid_encoding_p. UTF-8 validation uses utf8_strlen_check; binary strings are valid, single-byte state can be cached, and non-UTF-8 builds return TRUE.
Encoding predicate integration and tests
mrbgems/mruby-encoding/src/encoding.c, mrbgems/mruby-encoding/test/string.rb
Routes str_valid_enc_p through the shared API. Tests cover valid, invalid, truncated, binary, and shared-substring inputs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

  • mruby/mruby#7093: Both changes modify UTF-8 validation and related string encoding tests.
  • mruby/mruby#7095: Both changes touch the encoding-validation area in encoding.c.
  • mruby/mruby#7096: Both changes modify UTF-8 scanning and shared-substring boundary handling.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 moving String#valid_encoding? implementation into core as mrb_str_valid_encoding_p.
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants