Read what sprintf builds the way its bytes were read - #7143
Conversation
`Integer#chr` hands back a byte-read string for a byte above ASCII since 19d81d2, and the operations that build one string out of another carry the marking with the bytes since 7806a13 and 037f816. What sprintf builds does not. The bytes go through: a byte-read argument lands in the result whole, and a byte-read format string lays its own bytes down as they are. The reading that goes with them is left behind, so the result reports UTF-8 over bytes that refuse to read as it, one derivation away from the state `Integer#chr` stopped handing out. Whether a string is read as bytes or as UTF-8 is only visible through mruby-encoding, which this gem does not depend on, so a test asking the question skips itself in the state mrbtest builds for this gem. Every such test skips in every configuration, which leaves the answer unasserted rather than asserted somewhere else. Take the dependency in the test state when the build already carries the gem, the way mruby-regexp already does. A build without mruby-encoding is unchanged, and no build gains a gem it did not already have. Pin where every answer stands before any of it moves: the argument written through `%s`, `%c`, a width and a name, the format string's own bytes, and the three that say nothing about the reading either way.
What sprintf builds is the format string's own bytes with the arguments written between them, and it came back read as UTF-8 whatever went into it. A byte-read argument above ASCII lost its reading on the way through `%s`, which handed the result a claim its bytes could not honor: since 80c781b refuses a subject that does not read as UTF-8, `171.chr` matches against a pattern while `"%s" % [171.chr]`, the same byte, raises ArgumentError. Carry the format string's own reading into the result, the way a receiver's reading is carried through an append, and let an argument read as bytes and going above ASCII hand its reading over, the way appended byte-read bytes already do through `mrb_str_cat_str()`. `%c` writes an argument's bytes too and takes the same marking; an Integer argument is a code point rather than a byte and takes none, as does the string `%p` builds for itself. What an argument is read as is a property of the argument, so a precision that cuts the byte above ASCII off the written part moves nothing. This is where CRuby lands on every pair it accepts: it refuses the incompatible ones outright, where this says nothing rather than something false, as `+` and the append already do.
|
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 (3)
📝 WalkthroughWalkthroughThe ChangesSprintf encoding behavior
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🟡 Moderate · up to A formatted string can be incorrectly treated as byte-encoded even when precision removes the non-ASCII bytes that would justify that behavior, leading to incorrect encoding-sensitive results. Merge should wait for this bounded correctness issue to be fixed. 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 |
Follow-up to #7137, which named this seam among the ones it deliberately left out.
sprintfbuilds its result in a raw buffer and never looks at what the strings going into it were read as, so a byte-read argument loses that reading on the way through, and so does a byte-read format string:Since 80c781b refuses a subject whose bytes do not read as UTF-8, this is no longer a mis-indexing but a refusal:
171.chrmatches against a pattern, while"%s" % [171.chr], the same single byte, raisesArgumentError.The rule
The one #7137 settled, applied to the two places sprintf writes bytes it did not make itself.
Three sites: the result takes
MRB_STR_BINARYfrom the format string where it is created,%s/%pmarks from the string its argument converts to, and%cmarks from a String argument. An Integer argument to%cis a code point rather than a byte, and the string%pbuilds for itself is one of its own, so neither marks anything.What an argument is read as is a property of the argument, not of the part that reaches the buffer, so a precision cutting the byte above ASCII off the written part moves nothing:
"%.1s" % ["a\xABb".b]is byte-read, as in CRuby.This reproduces CRuby's answer for every pair it accepts. The pairs CRuby refuses with
Encoding::CompatibilityErrorcome out byte-read here rather than raising, the same choice #7137 made.Tests
The first commit pins where every answer stands before any of it moves; the second flips exactly the pins it changes.
Whether a string is read as bytes or as UTF-8 is only visible through mruby-encoding, which this gem does not depend on, so such a test skips itself in the state mrbtest builds for this gem, in every configuration. The first commit takes that dependency in the test state when the build already carries the gem, the way mruby-regexp does since #7136. A build without mruby-encoding is unchanged, and no build gains a gem it did not already have. The test asks
__ENCODING__as well, since a build that reads every string as bytes has nothing for the marking to tell apart.Still left out
Array#packandString#unpackmark nothing ([171].pack("C")reports UTF-8, invalid), and a Symbol has nowhere to keep the marking (171.chr.to_sym.to_sreports UTF-8). Each is its own change.Alongside #7142
#7142 leaves
MRB_UTF8_STRINGto the build, so carrying mruby-encoding stops meaning the build reads UTF-8, and a full-core build that indexes by byte runs in CI. That is what the__ENCODING__guard is for: redundant while the gem still sets the define, load-bearing once it does not. The two series share no file.MRB_STR_BINARYandRSTR_COPY_BINARY_FLAGdo not wait onMRB_UTF8_STRING, so what this one adds tosprintf.ccompiles and runs the same on either side; what changes is only whetherString#encodingshows the marking.Verification
Full suite green at every commit, on full-core with
MRB_UTF8_STRING(2279 tests), the same with the C++ ABI (2279), and the default gembox, where mruby-encoding is absent and the new test skips (2066). 0 failures, 0 crashes, no new compiler warnings.Merged with #7142, full-core without
MRB_UTF8_STRINGis green as well (2239), the new test among the skips. That series turns the C++ ABI build into one of those, so its count above moves there once it lands.Summary by CodeRabbit
sprintfandString#%handling of binary and UTF-8 strings.