string.c: a copy of a binary string is binary - #7080
Merged
Conversation
`String#b` marks a string byte-indexed with `MRB_STR_BINARY`, and
`str_replace()` copies `MRB_STR_SINGLE_BYTE` from the string it copies without
copying that one. `mrb_str_dup()` goes through it, so `dup`, `clone` and
`replace` all hand back a string holding the same bytes with nothing left to
say how to read them.
```ruby
s = "\u{1F600}".b # F0 9F 98 80: four bytes, one character
s.encoding # ASCII-8BIT
s.dup.encoding # was UTF-8, CRuby: ASCII-8BIT
s.clone.encoding # was UTF-8, CRuby: ASCII-8BIT
"x".replace(s) # was UTF-8, CRuby: ASCII-8BIT
```
The copy is not only mislabelled. Every offset computed from it switches to
counting characters, which a byte-indexed string does not have. `mruby-regexp`
reaches this without a copy in sight, because `create_matchdata()` snapshots
the subject with `mrb_str_dup_frozen()` so that a later change to the subject
is not visible through the `MatchData`. The snapshot is no longer binary, and
`re_byte_to_char()` then counts UTF-8 lead bytes over it:
```ruby
s = "\u{1F600}".b
s =~ Regexp.new("\x80") # was 1, CRuby: 3
s.byteindex(Regexp.new("\x80")) # 3, correct: no copy in the way
s.match(Regexp.new("\x80")).begin(0) # was 1
s.match(Regexp.new("\x80")).pre_match # three bytes, disagreeing with begin(0)
```
So one `MatchData` reported the same span two ways: `#pre_match` in bytes and
`#begin` in characters.
Copy the flag beside the one already copied. A copy holds the same bytes as
what it copies, so it is byte-indexed exactly when that is, and the two
neighbours `chars2bytes()` and `bytes2chars()` already read the flag rather
than the encoding of the moment.
`String#*`, the substring family and `String#+` have the same gap and are left
alone here: the first two want the same one-line treatment at their own sites,
and `+` takes two strings, so which side decides is a rule to choose rather
than a flag to copy.
|
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 (4)
📝 WalkthroughWalkthroughThe string flag API now copies binary-string metadata during replacement and copy operations. Encoding tests cover copied strings. Regexp tests verify byte-based offsets for byte-indexed strings and character-based offsets for regular UTF-8 strings. ChangesBinary flag preservation
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 13, 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.
str_replace()copiesMRB_STR_SINGLE_BYTEfrom the string it copies and notMRB_STR_BINARY, so every copy of a byte-indexed string comes back without theflag that says to read it as bytes.
Why it is more than a label
chars2bytes()andbytes2chars()both readMRB_STR_BINARYto decidewhether an index is a byte or a character, so a copy that lost the flag has
every offset computed off it switch units.
mruby-regexpreaches this without the caller making a copy at all.create_matchdata()snapshots the subject withmrb_str_dup_frozen(), so thatchanging the subject afterwards is not visible through the
MatchData. Thesnapshot loses the flag, and
re_byte_to_char()counts UTF-8 lead bytes over astring that holds no characters:
One
MatchDatareported the same span in two units.The change
Add
RSTR_COPY_BINARY_FLAGbesideRSTR_COPY_SINGLE_BYTE_FLAGand call it instr_replace(). A copy holds the same bytes as what it copies, so it isbyte-indexed exactly when that is. Three lines.
The flag is copied rather than set, so a copy of a UTF-8 string stays UTF-8 and
String#b, which dups and then sets the flag, is unaffected.Testing
rake testis green onfull-corewith gcc onx86_64-linux: 2216 asserts,no failures.
Both new tests fail without the change and pass with it:
A differential sweep against CRuby 4.0.6, over a byte-indexed subject with a
byte-indexed pattern (0x80 to 0xFF in four shapes, with and without
/i, tensubjects, 10240 cells), reading the position with
=~:The 84 this change closes are all offset reporting; the rest belong to #7078
and are unrelated to the flag.
Not in this change
String#*and the substring family have the same gap at their own sites, andString#+takes two strings, so which side decides is a rule to choose ratherthan a flag to copy.
String#sizereads a binary string as UTF-8 for adifferent reason:
utf8_strlen()does not ask about the flag, while its twoneighbours do.
Summary by CodeRabbit
Bug Fixes
String#byteindexandMatchData#begin/endnow return correct offsets for binary data.Tests