string.c: a binary string is measured in bytes - #7081
Merged
Conversation
`chars2bytes()` and `bytes2chars()` both ask `RSTR_BINARY_P()` before deciding
whether an index counts bytes or characters, so a string marked by `String#b`
is indexed by byte. `utf8_strlen()` asks only about the single-byte flag, so
the same string was measured as UTF-8. One string answered two ways, and the
length was the one every method bounded by it believed.
```ruby
s = "\u{1F600}".b # F0 9F 98 80: four bytes, one character
s.size # was 1, CRuby: 4
s.bytesize # 4
s[0] # "\xF0", indexed as a byte all along
s[1] # was nil, CRuby: "\x9F"
s[3] # was nil, CRuby: "\x80"
s[0, 2] # was "\xF0", CRuby: "\xF0\x9F"
s.slice(1, 2) # was "", CRuby: "\x9F\x98"
s.chars.size # 4, walking bytes and disagreeing with s.size
s.reverse! # was the string unchanged: one character reversed is itself
```
Ask the same question here. The single-byte flag is deliberately not set on the
way out: it says the bytes hold nothing multi-byte, while this returns early
because of how the string is read, and `force_encoding()` can hand the same
bytes back to the UTF-8 reading.
```ruby
s = "\u{1F600}".b
s.size # 4
s.force_encoding(Encoding::UTF_8)
s.size # 1
```
`String#reverse` still answers as before, because it copies the string first
and `str_replace()` does not carry `MRB_STR_BINARY` to the copy. `#reverse!`
is fixed here, and the copying form follows once the flag survives a copy.
`String#inspect` and `String#chop!` each walk `mrb_utf8len()` themselves rather
than going through the length, so a binary string is read as UTF-8 there for a
reason of their own, and they are left to their own fixes.
`mrb_str_index_m()` and `mrb_str_rindex_m()` take a byte path on the same flag
the others read, and are left alone: the general path they fall into converts
through `bytes2chars()`, which returns the byte offset for a binary string
already, so the answer is the same either way.
📝 WalkthroughWalkthrough
ChangesBinary string encoding behavior
Estimated code review effort: 2 (Simple) | ~10 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 10, 2026
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.
chars2bytes()andbytes2chars()both askRSTR_BINARY_P()before decidingwhether an index counts bytes or characters, so a string marked by
String#bis indexed by byte.
utf8_strlen()asks only about the single-byte flag, sothe same string is measured as UTF-8. One string answers two ways, and the
length is the one every method bounded by it believes.
The change
utf8_strlen()returns the byte length for a binary string, which is what itstwo neighbours already answer for the index. One condition.
The single-byte flag is deliberately not set on that path. That flag says the
bytes hold nothing multi-byte, while the early return is about how the string
is read, and
force_encodingcan hand the same bytes back to the UTF-8reading:
What this does not reach
String#reversecopies the string first, andstr_replace()does not carryMRB_STR_BINARYto the copy, so it still reads the copy as UTF-8.#reverse!is fixed here. #7080 makes the flag survive a copy, and with the two together
the copying form follows. Neither change depends on the other landing first,
and they merge cleanly in either order: they touch different functions in
src/string.cand add their tests at different points in the same file.String#inspectandString#chop!each walkmrb_utf8len()themselves ratherthan going through the length, so a binary string is read as UTF-8 there for a
reason of their own. Those are separate fixes and are not mixed in here.
mrb_str_index_m()andmrb_str_rindex_m()take a byte path on the same flagthe others read, and are left alone. The general path they fall into converts
through
bytes2chars(), which returns the byte offset for a binary stringalready, so the answer is the same either way and this would be a speed-up
rather than a fix.
Testing
rake testis green onfull-corewith gcc onx86_64-linux: 2216 asserts,no failures. Both new tests fail without the change:
Merged with #7080 locally,
rake testis green as well (2218 asserts), andString#reversejoins#reverse!in answering in bytes.Checked against CRuby 4.0.6 over
size,length,bytesize,[]by indexand by pair,
slice,chars,each_char,reverse!,center,ljust,rjustandinsert, on a four-byte one-character subject and on a mixed one:every case agrees afterwards except the ones listed above as out of reach.
Summary by CodeRabbit
Bug Fixes
Tests
String#reverse!.