diff --git a/NEWS.md b/NEWS.md index d0a4de6c836051..d36344025d544c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -176,7 +176,7 @@ They are still available on rubygems.org and can be installed with ### The following default gems are updated. * RubyGems 4.1.0.dev - * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17], [v4.0.18][RubyGems-v4.0.18], [v4.0.19][RubyGems-v4.0.19] + * 4.0.3 to [v4.0.4][RubyGems-v4.0.4], [v4.0.5][RubyGems-v4.0.5], [v4.0.6][RubyGems-v4.0.6], [v4.0.7][RubyGems-v4.0.7], [v4.0.8][RubyGems-v4.0.8], [v4.0.9][RubyGems-v4.0.9], [v4.0.10][RubyGems-v4.0.10], [v4.0.11][RubyGems-v4.0.11], [v4.0.12][RubyGems-v4.0.12], [v4.0.13][RubyGems-v4.0.13], [v4.0.14][RubyGems-v4.0.14], [v4.0.15][RubyGems-v4.0.15], [v4.0.16][RubyGems-v4.0.16], [v4.0.17][RubyGems-v4.0.17], [v4.0.18][RubyGems-v4.0.18], [v4.0.19][RubyGems-v4.0.19], [v4.0.20][RubyGems-v4.0.20] * bundler 4.1.0.dev * 4.0.3 to [v4.0.4][bundler-v4.0.4], [v4.0.5][bundler-v4.0.5], [v4.0.6][bundler-v4.0.6], [v4.0.7][bundler-v4.0.7], [v4.0.8][bundler-v4.0.8], [v4.0.9][bundler-v4.0.9], [v4.0.10][bundler-v4.0.10], [v4.0.11][bundler-v4.0.11], [v4.0.12][bundler-v4.0.12], [v4.0.13][bundler-v4.0.13], [v4.0.14][bundler-v4.0.14], [v4.0.15][bundler-v4.0.15], [v4.0.16][bundler-v4.0.16], [v4.0.17][bundler-v4.0.17] * erb 6.0.7 @@ -394,6 +394,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [RubyGems-v4.0.17]: https://github.com/rubygems/rubygems/releases/tag/v4.0.17 [RubyGems-v4.0.18]: https://github.com/rubygems/rubygems/releases/tag/v4.0.18 [RubyGems-v4.0.19]: https://github.com/rubygems/rubygems/releases/tag/v4.0.19 +[RubyGems-v4.0.20]: https://github.com/rubygems/rubygems/releases/tag/v4.0.20 [bundler-v4.0.4]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.4 [bundler-v4.0.5]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.5 [bundler-v4.0.6]: https://github.com/rubygems/rubygems/releases/tag/bundler-v4.0.6 diff --git a/benchmark/string_tr.yml b/benchmark/string_tr.yml new file mode 100644 index 00000000000000..e0f582c240c17b --- /dev/null +++ b/benchmark/string_tr.yml @@ -0,0 +1,21 @@ +prelude: | + # frozen_string_literal: true + + require 'cgi/escape' + + STR = ((("a" * 31) + "<") * 1000).freeze + + ESCAPED_CHARS = { + ">" => '\u003e', + "<" => '\u003c', + "&" => '\u0026', + }.freeze + ESCAPE_PATTERN = Regexp.union(ESCAPED_CHARS.keys) + +benchmark: + tr_escape: | + STR.tr(ESCAPED_CHARS) + gsub_escape: | + STR.gsub(ESCAPE_PATTERN, ESCAPED_CHARS) + cgi_escape: | + CGI.escape_html(STR) diff --git a/encoding.c b/encoding.c index a49f3b6b3adc0f..30cee45fd2b3e8 100644 --- a/encoding.c +++ b/encoding.c @@ -1198,6 +1198,47 @@ enc_compatible_latter(VALUE str1, VALUE str2, int idx1, int idx2) return 0; } +rb_encoding * +rb_enc_check_multi_str(rb_encoding *enc1, int *cr, VALUE str2) +{ + RUBY_ASSERT(*cr != ENC_CODERANGE_UNKNOWN); + + if (RSTRING_LEN(str2) == 0) { + return enc1; + } + + int idx2 = enc_get_index_str(str2); + rb_encoding *enc2 = rb_enc_from_index(idx2); + + int cr1 = *cr; + + if (enc1 == enc2) { + if (cr1 == ENC_CODERANGE_7BIT) { + *cr = rb_enc_str_coderange(str2); + } + return enc1; + } + + if (!rb_enc_asciicompat(enc1) || !rb_enc_asciicompat(enc2)) { + rb_raise(rb_eEncCompatError, "incompatible character encodings: %s and %s", + rb_enc_inspect_name(enc1), + rb_enc_inspect_name(enc2)); + } + + if (enc2 == global_enc_us_ascii || rb_enc_str_asciionly_p(str2)) { + return enc1; + } + + if (enc1 == global_enc_us_ascii || cr1 == ENC_CODERANGE_7BIT) { + *cr = rb_enc_str_coderange(str2); + return enc2; + } + + rb_raise(rb_eEncCompatError, "incompatible character encodings: %s and %s", + rb_enc_inspect_name(enc1), + rb_enc_inspect_name(enc2)); +} + static rb_encoding* enc_compatible_str(VALUE str1, VALUE str2) { diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index 38607cf4eb0b6d..d0d04875122472 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -1124,15 +1124,6 @@ static void generate_json_fallback(FBuffer *buffer, struct generate_json_data *d } } -static inline void generate_json_symbol(FBuffer *buffer, struct generate_json_data *data, VALUE obj) -{ - if (data->state->strict) { - generate_json_string(buffer, data, rb_sym2str(obj)); - } else { - generate_json_fallback(buffer, data, obj); - } -} - static void generate_json_null(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { fbuffer_append(buffer, "null", 4); @@ -1218,7 +1209,13 @@ static inline void generate_json_general(FBuffer *buffer, struct generate_json_d } else if (RB_FLONUM_P(obj)) { generate_json_float(buffer, data, obj); } else if (RB_STATIC_SYM_P(obj)) { - generate_json_symbol(buffer, data, obj); + if (data->state->strict) { + obj = rb_sym2str(obj); + JSON_ASSERT(RBASIC_CLASS(obj) == rb_cString); + goto generate_string; + } + + generate_json_fallback(buffer, data, obj); } else { goto general; } @@ -1239,6 +1236,7 @@ static inline void generate_json_general(FBuffer *buffer, struct generate_json_d case T_STRING: if (fallback && klass != rb_cString) goto general; + generate_string: if (RB_LIKELY(valid_json_string_p(obj))) { raw_generate_json_string(buffer, data, obj); } else if (as_json_called) { @@ -1250,7 +1248,13 @@ static inline void generate_json_general(FBuffer *buffer, struct generate_json_d } break; case T_SYMBOL: - generate_json_symbol(buffer, data, obj); + if (data->state->strict) { + obj = rb_sym2str(obj); + JSON_ASSERT(RBASIC_CLASS(obj) == rb_cString); + goto generate_string; + } + + generate_json_fallback(buffer, data, obj); break; case T_FLOAT: if (fallback && klass != rb_cFloat) goto general; diff --git a/internal/encoding.h b/internal/encoding.h index 38bf8fc9da77ef..09c125804f3fc8 100644 --- a/internal/encoding.h +++ b/internal/encoding.h @@ -30,6 +30,7 @@ void rb_enc_set_base(const char *name, const char *orig); int rb_enc_set_dummy(int index); void rb_enc_raw_set(VALUE obj, rb_encoding *enc); int rb_enc_registered(const char *name); +rb_encoding *rb_enc_check_multi_str(rb_encoding *enc1, int *cr, VALUE str2); PUREFUNC(int rb_data_is_encoding(VALUE obj)); diff --git a/lib/bundler/bundler.gemspec b/lib/bundler/bundler.gemspec index 346d156693e018..812cf74bc5333e 100644 --- a/lib/bundler/bundler.gemspec +++ b/lib/bundler/bundler.gemspec @@ -55,7 +55,7 @@ Gem::Specification.new do |s| # These live next to the gemspec when Bundler ships as a gem, but not when # it is synced into Ruby core, where the gemspec moves under lib/bundler. - s.files += %w[CHANGELOG-bundler.md LICENSE-bundler.md README-bundler.md].select {|f| File.file?(f) } + s.files += %w[CHANGELOG-bundler.md MIT.txt].select {|f| File.file?(f) } s.bindir = "exe" s.executables = %w[bundle bundler] s.require_paths = ["lib"] diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb index be9bf76e83b87a..a8eadcb6498bde 100644 --- a/lib/rubygems/util/atomic_file_writer.rb +++ b/lib/rubygems/util/atomic_file_writer.rb @@ -53,9 +53,9 @@ def self.open(file_name) if old_stat # Set correct permissions on new file begin - File.chown(old_stat.uid, old_stat.gid, tmp_path) + temp_file.chown(old_stat.uid, old_stat.gid) # This operation will affect filesystem ACL's - File.chmod(old_stat.mode, tmp_path) + temp_file.chmod(old_stat.mode) rescue Errno::EPERM, Errno::EACCES # Changing file ownership failed, moving on. end diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb index 5c107758518078..5df6de58d6cfe6 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -359,7 +359,7 @@ def git_ls_files(glob) end def tracked_files_glob - ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* spec/bundler man/bundle*" : "exe/bundle exe/bundler lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* bundler.gemspec CHANGELOG-bundler.md LICENSE-bundler.md README-bundler.md" + ruby_core? ? "libexec/bundle* lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* spec/bundler man/bundle*" : "exe/bundle exe/bundler lib/bundler lib/bundler.rb lib/rubygems/vendor/uri lib/rubygems/vendor/securerandom lib/rubygems/vendor/pub_grub lib/rubygems/yaml_serializer.rb lib/rubygems/compact_index_client* lib/rubygems/credential_store* bundler.gemspec CHANGELOG-bundler.md MIT.txt" end def lib_tracked_files_glob diff --git a/spec/ruby/core/string/tr_spec.rb b/spec/ruby/core/string/tr_spec.rb index cb57c3851eec5e..98551d4ab83b3e 100644 --- a/spec/ruby/core/string/tr_spec.rb +++ b/spec/ruby/core/string/tr_spec.rb @@ -93,6 +93,50 @@ str.tr(a, b).should == "椎名深夏" end + it "raises Encoding::CompatibilityError when from_string or to_string parameters have an incompatible encoding" do + -> { "fée".tr("é".encode(Encoding::ISO_8859_1), "e") }.should.raise(Encoding::CompatibilityError) + -> { "fée".tr("e", "é".encode(Encoding::ISO_8859_1)) }.should.raise(Encoding::CompatibilityError) + end + + ruby_version_is "4.1" do + describe "hash form" do + it "returns a new strings with characters from keys replaced by the associated strings" do + "hello".tr("e" => "EH! ", "o" => "OH!", "l" => "").should == "hEH! OH!" + "h€llø".tr("€" => "e", "ø" => "o").should == "hello" + end + + it "returns a string in the combined encoding" do + str = "hello".encode(Encoding::US_ASCII).tr("e" => "é") + str.should == "héllo" + str.encoding.should == Encoding::UTF_8 + end + + it "works with multi-byte encodings" do + "hello".encode(Encoding::UTF_16LE).tr( + "e".encode(Encoding::UTF_16LE) => "EH! ".encode(Encoding::UTF_16LE), + "o".encode(Encoding::UTF_16LE) => "OH!".encode(Encoding::UTF_16LE), + "l".encode(Encoding::UTF_16LE) => "".encode(Encoding::UTF_16LE), + ).should == "hEH! OH!".encode(Encoding::UTF_16LE) + + "h€llø".encode(Encoding::UTF_16LE).tr( + "€".encode(Encoding::UTF_16LE) => "e".encode(Encoding::UTF_16LE), + "ø".encode(Encoding::UTF_16LE) => "o".encode(Encoding::UTF_16LE), + ).should == "hello".encode(Encoding::UTF_16LE) + end + + it "raises ArgumentError if a key is more than one codepoint" do + -> { "hello".tr("hel" => "") }.should.raise(ArgumentError) + -> { "🤦🏼‍♂️".tr("🤦🏼‍♂️" => "") }.should.raise(ArgumentError) + end + + it "raises Encoding::CompatibilityError when either keys or values have an incompatible encoding" do + -> { "fée".tr("é".encode(Encoding::ISO_8859_1) => "e") }.should.raise(Encoding::CompatibilityError) + -> { "fée".tr("e" => "é".encode(Encoding::ISO_8859_1)) }.should.raise(Encoding::CompatibilityError) + + -> { "ab".encode(Encoding::US_ASCII).tr("a" => "à".encode(Encoding::ISO_8859_1), "b" => "é") }.should.raise(Encoding::CompatibilityError) + end + end + end end describe "String#tr!" do @@ -123,4 +167,48 @@ -> { s.tr!("R", "S") }.should.raise(FrozenError) -> { s.tr!("", "") }.should.raise(FrozenError) end + + ruby_version_is "4.1" do + describe "hash form" do + it "returns a new strings with characters from keys replaced by the associated strings" do + "hello".tr!("e" => "EH! ", "o" => "OH!", "l" => "").should == "hEH! OH!" + "h€llø".tr!("€" => "e", "ø" => "o").should == "hello" + end + + it "returns a string in the combined encoding" do + str = "hello".encode(Encoding::US_ASCII).tr!("e" => "é") + str.should == "héllo" + str.encoding.should == Encoding::UTF_8 + end + + it "works with multi-byte encodings" do + "hello".encode(Encoding::UTF_16LE).tr!( + "e".encode(Encoding::UTF_16LE) => "EH! ".encode(Encoding::UTF_16LE), + "o".encode(Encoding::UTF_16LE) => "OH!".encode(Encoding::UTF_16LE), + "l".encode(Encoding::UTF_16LE) => "".encode(Encoding::UTF_16LE), + ).should == "hEH! OH!".encode(Encoding::UTF_16LE) + + "h€llø".encode(Encoding::UTF_16LE).tr!( + "€".encode(Encoding::UTF_16LE) => "e".encode(Encoding::UTF_16LE), + "ø".encode(Encoding::UTF_16LE) => "o".encode(Encoding::UTF_16LE), + ).should == "hello".encode(Encoding::UTF_16LE) + end + + it "returns nil if the string wasn't modified" do + "hello".tr!("€" => "", "Ø" => "").should == nil + end + + it "raises ArgumentError if a key is more than one codepoint" do + -> { "hello".tr!("hel" => "") }.should.raise(ArgumentError) + -> { "🤦🏼‍♂️".tr!("🤦🏼‍♂️" => "") }.should.raise(ArgumentError) + end + + it "raises Encoding::CompatibilityError when either keys or values have an incompatible encoding" do + -> { "fée".tr!("é".encode(Encoding::ISO_8859_1) => "e") }.should.raise(Encoding::CompatibilityError) + -> { "fée".tr!("e" => "é".encode(Encoding::ISO_8859_1)) }.should.raise(Encoding::CompatibilityError) + + -> { "ab".encode(Encoding::US_ASCII).tr!("a" => "à".encode(Encoding::ISO_8859_1), "b" => "é") }.should.raise(Encoding::CompatibilityError) + end + end + end end diff --git a/string.c b/string.c index e4503a96bf1f2d..11534c0ca1157a 100644 --- a/string.c +++ b/string.c @@ -9186,10 +9186,274 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) return Qnil; } +struct tr_buffer { + unsigned char *buf; + unsigned char *ptr; + size_t capa; + size_t initial_capa; +}; + +static inline void +tr_buffer_init(struct tr_buffer *buffer, size_t initial_capa) +{ + if (initial_capa < 32) { + initial_capa = 32; + } + *buffer = (struct tr_buffer){ .initial_capa = initial_capa }; +} + +static inline void +tr_buffer_ensure_capa(struct tr_buffer *buffer, size_t extra_capa) +{ + size_t offset = buffer->ptr - buffer->buf; + size_t required_capa = offset + extra_capa; + if (UNLIKELY(buffer->capa < required_capa)) { + size_t new_capa = buffer->capa ? buffer->capa : buffer->initial_capa; + while (new_capa < required_capa) { + new_capa *= 1.2; + } + buffer->buf = SIZED_REALLOC_N(buffer->buf, unsigned char, new_capa, buffer->capa); + buffer->ptr = buffer->buf + offset; + buffer->capa = new_capa; + } +} + +static inline void +tr_buffer_append(struct tr_buffer *buffer, unsigned char *ptr, size_t len) +{ + if (len) { + tr_buffer_ensure_capa(buffer, len); + memcpy(buffer->ptr, ptr, len); + buffer->ptr += len; + } +} + +static inline void +tr_buffer_append_str(struct tr_buffer *buffer, VALUE str) +{ + tr_buffer_append(buffer, (unsigned char *)RSTRING_PTR(str), RSTRING_LEN(str)); +} + +static inline void +tr_buffer_mbcput(struct tr_buffer *buffer, int codepoint, rb_encoding *enc) +{ + tr_buffer_ensure_capa(buffer, 4); + buffer->ptr += rb_enc_mbcput(codepoint, buffer->ptr, enc); +} + +static inline void +tr_buffer_free(struct tr_buffer *buffer) +{ + if (buffer->buf) { + SIZED_FREE_N(buffer->buf, buffer->capa); + } +} + +struct tr_pair { + VALUE search; + VALUE replace; +}; + +struct tr_trans_pairs_coerce_args { + struct tr_pair *pairs; + size_t index; + rb_encoding *enc; + int cr; +}; + +static int +tr_trans_pairs_coerce_i(st_data_t key, st_data_t value, st_data_t _args) +{ + struct tr_trans_pairs_coerce_args *args = (struct tr_trans_pairs_coerce_args *)_args; + struct tr_pair *pair = &args->pairs[args->index]; + args->index++; + + VALUE search = (VALUE)key; + VALUE replace = (VALUE)value; + StringValue(search); + StringValue(replace); + + if (RSTRING_LEN(search) != 1 && str_strlen(search, NULL) != 1) { + rb_raise(rb_eArgError, "keys must be of size 1"); // TODO: better error message + } + + args->enc = rb_enc_check_multi_str(args->enc, &args->cr, search); + args->enc = rb_enc_check_multi_str(args->enc, &args->cr, replace); + + pair->search = search; + pair->replace = replace; + return ST_CONTINUE; +} + +static VALUE +tr_trans_pairs(VALUE str, VALUE pairs_val) +{ + Check_Type(pairs_val, T_HASH); + size_t pairs_count = RHASH_SIZE(pairs_val); + rb_str_modify(str); + + if (RSTRING_LEN(str) == 0 || !RSTRING_PTR(str) || pairs_count == 0) return Qnil; + + VALUE pairs_handle; + struct tr_pair *pairs = ALLOCV_N(struct tr_pair, pairs_handle, pairs_count); + + int cr = rb_enc_str_coderange(str); + rb_encoding *enc = rb_str_enc_get(str); + + struct tr_trans_pairs_coerce_args coerce_args = { + .pairs = pairs, + .enc = enc, + .cr = cr, + }; + rb_hash_foreach(pairs_val, tr_trans_pairs_coerce_i, (VALUE)&coerce_args); + rb_encoding *e1 = coerce_args.enc; + + VALUE hash = 0; + + unsigned char *sstart = (unsigned char *)RSTRING_PTR(str); + unsigned char *s = sstart; + unsigned char *send = sstart + RSTRING_LEN(str); + int termlen = rb_enc_mbminlen(e1); + + struct tr_buffer buffer; + tr_buffer_init(&buffer, send - s); + bool modify = false; + + if (RB_LIKELY(rb_str_encindex_fastpath(rb_enc_to_index(e1)))) { + VALUE trans_table[256] = { 0 }; + + for (size_t index = 0; index < pairs_count; index++) { + struct tr_pair *pair = &pairs[index]; + + char *ptr = RSTRING_PTR(pair->search); + unsigned int codepoint = rb_enc_mbc_to_codepoint(ptr, RSTRING_END(pair->search), e1); + + if (rb_enc_codelen(codepoint, e1) == 1) { + trans_table[(unsigned char)*ptr] = pair->replace; + } + else { + trans_table[(unsigned char)*ptr] = Qundef; + if (!hash) { + hash = rb_obj_hide(rb_hash_new_capa(pairs_count)); + } + rb_hash_aset(hash, UINT2NUM(codepoint), pair->replace); + } + } + + unsigned char *checkpoint = s; + while (s < send) { + VALUE repl = trans_table[*s]; + + int clen = 1; + + if (UNLIKELY(repl == Qundef)) { + unsigned int c = rb_enc_mbc_to_codepoint((char *)s, (char *)send, e1); + clen = rb_enc_codelen(c, e1); + repl = rb_hash_lookup2(hash, UINT2NUM(c), 0); + } + + if (LIKELY(repl == 0)) { + s += clen; + continue; + } + + modify = true; + + if (checkpoint < s) { + tr_buffer_append(&buffer, checkpoint, s - checkpoint); + } + tr_buffer_append_str(&buffer, repl); + s += clen; + checkpoint = s; + + if (cr == ENC_CODERANGE_7BIT && rb_enc_str_coderange(repl) != ENC_CODERANGE_7BIT) { + cr == ENC_CODERANGE_VALID; + } + } + + if (modify && checkpoint < s) { + tr_buffer_append(&buffer, checkpoint, s - checkpoint); + } + } + else { + hash = rb_obj_hide(rb_hash_new_capa(pairs_count)); + + for (size_t index = 0; index < pairs_count; index++) { + struct tr_pair *pair = &pairs[index]; + + unsigned int codepoint = rb_enc_mbc_to_codepoint(RSTRING_PTR(pair->search), RSTRING_END(pair->search), e1); + rb_hash_aset(hash, UINT2NUM(codepoint), pair->replace); + } + + while (s < send) { + bool may_modify = false; + + int r = rb_enc_precise_mbclen((char *)s, (char *)send, e1); + if (!MBCLEN_CHARFOUND_P(r)) { + tr_buffer_free(&buffer); + rb_raise(rb_eArgError, "invalid byte sequence in %s", rb_enc_name(e1)); + } + int clen = MBCLEN_CHARFOUND_LEN(r); + unsigned int c = rb_enc_mbc_to_codepoint((char *)s, (char *)send, e1); + unsigned int c0 = c; + + long tlen = enc == e1 ? clen : rb_enc_codelen(c, e1); + + VALUE replacement = rb_hash_lookup(hash, UINT2NUM(c)); + if (NIL_P(replacement)) { + tlen = enc == e1 ? clen : rb_enc_codelen(c, enc); + c = c0; + if (enc != e1) may_modify = true; + } + else { + tlen = RSTRING_LEN(replacement); + modify = true; + } + + if (NIL_P(replacement)) { + tr_buffer_mbcput(&buffer, c, enc); + } + else { + tr_buffer_append_str(&buffer, replacement); + } + + if (may_modify && memcmp(s, buffer.ptr - tlen, tlen) != 0) { + modify = true; + } + + if (cr == ENC_CODERANGE_7BIT && !rb_isascii(c)) { + cr = ENC_CODERANGE_VALID; + } + + s += clen; + } + } + + if (!STR_EMBED_P(str)) { + SIZED_FREE_N(STR_HEAP_PTR(str), STR_HEAP_SIZE(str)); + } + tr_buffer_ensure_capa(&buffer, termlen); + TERM_FILL((char *)buffer.ptr, termlen); + RSTRING(str)->as.heap.ptr = (char *)buffer.buf; + STR_SET_LEN(str, buffer.ptr - buffer.buf); + STR_SET_NOEMBED(str); + RSTRING(str)->as.heap.aux.capa = buffer.capa - termlen; + + RB_GC_GUARD(hash); + + if (modify) { + if (cr != ENC_CODERANGE_BROKEN) + ENC_CODERANGE_SET(str, cr); + rb_enc_associate(str, e1); + return str; + } + return Qnil; +} /* * call-seq: * tr!(selector, replacements) -> self or nil + * tr!(pairs) -> self or nil * * Like String#tr, except: * @@ -9200,8 +9464,16 @@ tr_trans(VALUE str, VALUE src, VALUE repl, int sflag) */ static VALUE -rb_str_tr_bang(VALUE str, VALUE src, VALUE repl) +rb_str_tr_bang(int argc, VALUE *argv, VALUE str) { + rb_check_arity(argc, 1, 2); + + if (argc == 1) { + VALUE pairs = argv[0]; + return tr_trans_pairs(str, pairs); + } + + VALUE src = argv[0], repl = argv[1]; return tr_trans(str, src, repl, 0); } @@ -9244,9 +9516,18 @@ rb_str_tr_bang(VALUE str, VALUE src, VALUE repl) */ static VALUE -rb_str_tr(VALUE str, VALUE src, VALUE repl) +rb_str_tr(int argc, VALUE *argv, VALUE str) { + rb_check_arity(argc, 1, 2); + str = str_duplicate(rb_cString, str); + + if (argc == 1) { + VALUE pairs = argv[0]; + return tr_trans_pairs(str, pairs); + } + + VALUE src = argv[0], repl = argv[1]; tr_trans(str, src, repl, 0); return str; } @@ -13535,13 +13816,13 @@ Init_String(void) rb_define_method(rb_cString, "delete_prefix!", rb_str_delete_prefix_bang, 1); rb_define_method(rb_cString, "delete_suffix!", rb_str_delete_suffix_bang, 1); - rb_define_method(rb_cString, "tr", rb_str_tr, 2); + rb_define_method(rb_cString, "tr", rb_str_tr, -1); rb_define_method(rb_cString, "tr_s", rb_str_tr_s, 2); rb_define_method(rb_cString, "delete", rb_str_delete, -1); rb_define_method(rb_cString, "squeeze", rb_str_squeeze, -1); rb_define_method(rb_cString, "count", rb_str_count, -1); - rb_define_method(rb_cString, "tr!", rb_str_tr_bang, 2); + rb_define_method(rb_cString, "tr!", rb_str_tr_bang, -1); rb_define_method(rb_cString, "tr_s!", rb_str_tr_s_bang, 2); rb_define_method(rb_cString, "delete!", rb_str_delete_bang, -1); rb_define_method(rb_cString, "squeeze!", rb_str_squeeze_bang, -1); diff --git a/test/json/json_coder_test.rb b/test/json/json_coder_test.rb index a8477dd7be89b1..7be1dfb3f301be 100755 --- a/test/json/json_coder_test.rb +++ b/test/json/json_coder_test.rb @@ -137,6 +137,33 @@ def test_json_coder_string_invalid_encoding assert_equal 2, calls end + def test_json_coder_symbol_invalid_encoding + symbol = "\xFF".b.to_sym + + calls = 0 + coder = JSON::Coder.new do |object, is_key| + calls += 1 + object.bytes + end + + assert_equal "[[255]]", coder.dump([symbol]) + assert_equal 1, calls + + coder = JSON::Coder.new { |object, is_key| nil } + assert_equal "[null]", coder.dump([symbol]) + + coder = JSON::Coder.new { |object, is_key| Object.new } + assert_raise(JSON::GeneratorError) { coder.dump([symbol]) } + + if RUBY_ENGINE == "ruby" + coder = JSON::Coder.new { |object, is_key| symbol } + error = assert_raise JSON::GeneratorError do + coder.dump([symbol]) + end + assert_equal "source sequence is illegal/malformed utf-8", error.message + end + end + def test_depth coder = JSON::Coder.new(object_nl: "\n", array_nl: "\n", space: " ", indent: " ", depth: 1) assert_equal %({\n "foo": 42\n }), coder.dump(foo: 42) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b567c8d2794c84..f66bddd4eb4332 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -7085,9 +7085,18 @@ impl Function { /// * It must not reference a FrameState `Snapshot` operand: a side exit /// materializes the enclosing inlined frame, and effects don't model /// deopt for otherwise pure instructions like `FixnumAdd`. + /// (`Snapshot` instructions themselves are exempt: they are inert data + /// that cannot side-exit, so they should not prevent elision.) /// * `LoadSP` reads the frame-dependent SP register despite having empty /// effects, so it's excluded explicitly. fn can_elide_enclosing_frame(&self, insn: &Insn) -> bool { + // A `Snapshot` only matters as the deopt state of an instruction + // that can side-exit. It shouldn't prevent elision on its own. + // Side-exiting instructions will block elision when they're scanned. + // TODO (nirvdrum 2026-09-02) Replace this specific instruction check with a check of the instruction's effects. + if matches!(insn, Insn::Snapshot { .. }) { + return true; + } // TODO: Model LoadSP as reading from the control frame and drop this // special case. if matches!(insn, Insn::LoadSP) { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index f8a8fd6d0f7a14..62ab8c6bd66096 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2146,6 +2146,43 @@ mod hir_opt_tests { "); } + #[test] + fn test_eliminate_empty_inline_frames_with_dead_snapshot() { + eval(" + def add(a, b) = a + b + def test = add(1, 2) + add(3, 4) + test + "); + + // `add` is inlined at both call sites, giving two `PushInlineFrame`/`PopInlineFrame` + // pairs. The first pair keeps `add`'s `PatchPoint` and `CheckInterrupts`, which the + // deduplication passes leave on the earliest copy, so it has real work between it and + // survives. The second pair and its body are optimized away entirely. + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:Fixnum[1] = Const Value(1) + v13:Fixnum[2] = Const Value(2) + PatchPoint MethodRedefined(Object@0x1000, add@0x1008, cme:0x1010) + v32:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + PushInlineFrame :add, v32 (0x1038), num_args=2 + PatchPoint MethodRedefined(Integer@0x1058, +@0x1060, cme:0x1068) + v88:Fixnum[3] = Const Value(3) + CheckInterrupts + PopInlineFrame + v90:Fixnum[10] = Const Value(10) + Return v90 + "); + } + #[test] fn test_call_with_correct_and_too_many_args_for_method() { eval("