Skip to content

string.c: a binary string is measured in bytes - #7081

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:string-binary-length
Aug 10, 2026
Merged

string.c: a binary string is measured in bytes#7081
matz merged 1 commit into
mruby:masterfrom
takumin:string-binary-length

Conversation

@takumin

@takumin takumin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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 is measured as UTF-8. One string answers two ways, and the
length is the one every method bounded by it believes.

s = "\u{1F600}".b   # F0 9F 98 80: four bytes, one character

s.size          # 1     <- CRuby: 4
s.bytesize      # 4
s[0]            # "\xF0"   indexed as a byte all along
s[1]            # nil   <- CRuby: "\x9F"
s[3]            # nil   <- CRuby: "\x80"
s[0, 2]         # "\xF0"   <- CRuby: "\xF0\x9F"
s.slice(1, 2)   # ""    <- CRuby: "\x9F\x98"
s.chars.size    # 4, walking bytes and disagreeing with s.size
s.reverse!      # the string unchanged: one character reversed is itself

The change

utf8_strlen() returns the byte length for a binary string, which is what its
two 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_encoding can hand the same bytes back to the UTF-8
reading:

s = "\u{1F600}".b
s.size                             # 4
s.force_encoding(Encoding::UTF_8)
s.size                             # 1, not a cached 4

What this does not reach

String#reverse copies the string first, and str_replace() does not carry
MRB_STR_BINARY to 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.c and add their tests at different points in the same file.

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. Those are separate fixes and are not mixed in here.

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 and this would be a speed-up
rather than a fix.

Testing

rake test is green on full-core with gcc on x86_64-linux: 2216 asserts,
no failures. Both new tests fail without the change:

Fail: String#length of a binary string counts bytes
Fail: String#reverse! on a binary string reverses bytes
  KO: 2   ->   KO: 0

Merged with #7080 locally, rake test is green as well (2218 asserts), and
String#reverse joins #reverse! in answering in bytes.

Checked against CRuby 4.0.6 over size, length, bytesize, [] by index
and by pair, slice, chars, each_char, reverse!, center, ljust,
rjust and insert, 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

    • Corrected string length handling for binary strings so lengths are reported in bytes.
    • Improved behavior for indexing, slicing, encoding changes, and reversing binary and multibyte UTF-8 strings.
  • Tests

    • Added coverage for UTF-8 string operations, including byte-based lengths and String#reverse!.

`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.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

utf8_strlen now returns byte lengths for binary strings. Encoding tests cover byte-based length, indexing, slicing, encoding changes, and reversal for binary and UTF-8 strings.

Changes

Binary string encoding behavior

Layer / File(s) Summary
Binary string length and reversal behavior
src/string.c, mrbgems/mruby-encoding/test/string.rb
utf8_strlen returns byte_len for strings marked MRB_STR_BINARY. Tests validate byte-based length, indexing, slicing, encoding changes, and byte or character reversal behavior.

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

Possibly related PRs

  • mruby/mruby#7080: Modifies src/string.c and encoding tests involving MRB_STR_BINARY.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: measuring binary strings by byte length in string.c.
✨ 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 16a8415 into mruby:master Aug 10, 2026
21 checks passed
@takumin
takumin deleted the string-binary-length branch August 10, 2026 23:31
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