diff --git a/CHANGES.md b/CHANGES.md index 665635bb..9466fb5e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,22 @@ # Changes +### 2024-11-04 (2.7.6) + +* Fix a regression in JSON.generate when dealing with Hash keys that are string subclasses, call `to_json` on them. + +### 2024-10-25 (2.7.5) + +* Fix a memory leak when `#to_json` methods raise an exception. +* Gracefully handle formatting configs being set to `nil` instead of `""`. +* Workaround another issue caused by conflicting versions of both `json_pure` and `json` being loaded. + +### 2024-10-25 (2.7.4) + +* Workaround a bug in 3.4.8 and older https://github.com/rubygems/rubygems/pull/6490. + This bug would cause some gems with native extension to fail during compilation. +* Workaround different versions of `json` and `json_pure` being loaded (not officially supported). +* Make `json_pure` Ractor compatible. + ### 2024-10-24 (2.7.3) * Numerous performance optimizations in `JSON.generate` and `JSON.dump` (up to 2 times faster). diff --git a/ext/json/ext/fbuffer/fbuffer.h b/ext/json/ext/fbuffer/fbuffer.h index 76bd6ce1..4c62aa36 100644 --- a/ext/json/ext/fbuffer/fbuffer.h +++ b/ext/json/ext/fbuffer/fbuffer.h @@ -36,6 +36,10 @@ static VALUE fbuffer_to_s(FBuffer *fb); #define RB_UNLIKELY(expr) expr #endif +#ifndef RB_LIKELY +#define RB_LIKELY(expr) expr +#endif + static FBuffer *fbuffer_alloc(unsigned long initial_length) { FBuffer *fb; diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index c35e86d9..a04df36d 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -403,7 +403,9 @@ static char *fstrndup(const char *ptr, unsigned long len) { */ static VALUE mHash_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(object); + rb_check_arity(argc, 0, 1); + VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); + return cState_partial_generate(Vstate, self, generate_json_object); } /* @@ -415,7 +417,9 @@ static VALUE mHash_to_json(int argc, VALUE *argv, VALUE self) * produced JSON string output further. */ static VALUE mArray_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(array); + rb_check_arity(argc, 0, 1); + VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); + return cState_partial_generate(Vstate, self, generate_json_array); } #ifdef RUBY_INTEGER_UNIFICATION @@ -426,7 +430,9 @@ static VALUE mArray_to_json(int argc, VALUE *argv, VALUE self) { */ static VALUE mInteger_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(integer); + rb_check_arity(argc, 0, 1); + VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); + return cState_partial_generate(Vstate, self, generate_json_integer); } #else @@ -437,7 +443,9 @@ static VALUE mInteger_to_json(int argc, VALUE *argv, VALUE self) */ static VALUE mFixnum_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(fixnum); + rb_check_arity(argc, 0, 1); + VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); + return cState_partial_generate(Vstate, self, generate_json_fixnum); } /* @@ -447,7 +455,9 @@ static VALUE mFixnum_to_json(int argc, VALUE *argv, VALUE self) */ static VALUE mBignum_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(bignum); + rb_check_arity(argc, 0, 1); + VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); + return cState_partial_generate(Vstate, self, generate_json_bignum); } #endif @@ -458,7 +468,9 @@ static VALUE mBignum_to_json(int argc, VALUE *argv, VALUE self) */ static VALUE mFloat_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(float); + rb_check_arity(argc, 0, 1); + VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); + return cState_partial_generate(Vstate, self, generate_json_float); } /* @@ -481,7 +493,9 @@ static VALUE mString_included_s(VALUE self, VALUE modul) { */ static VALUE mString_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(string); + rb_check_arity(argc, 0, 1); + VALUE Vstate = cState_from_state_s(cState, argc == 1 ? argv[0] : Qnil); + return cState_partial_generate(Vstate, self, generate_json_string); } /* @@ -536,7 +550,8 @@ static VALUE mString_Extend_json_create(VALUE self, VALUE o) */ static VALUE mTrueClass_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(true); + rb_check_arity(argc, 0, 1); + return rb_utf8_str_new("true", 4); } /* @@ -546,7 +561,8 @@ static VALUE mTrueClass_to_json(int argc, VALUE *argv, VALUE self) */ static VALUE mFalseClass_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(false); + rb_check_arity(argc, 0, 1); + return rb_utf8_str_new("false", 5); } /* @@ -556,7 +572,8 @@ static VALUE mFalseClass_to_json(int argc, VALUE *argv, VALUE self) */ static VALUE mNilClass_to_json(int argc, VALUE *argv, VALUE self) { - GENERATE_JSON(null); + rb_check_arity(argc, 0, 1); + return rb_utf8_str_new("null", 4); } /* @@ -573,7 +590,7 @@ static VALUE mObject_to_json(int argc, VALUE *argv, VALUE self) rb_scan_args(argc, argv, "01", &state); Check_Type(string, T_STRING); state = cState_from_state_s(cState, state); - return cState_partial_generate(state, string); + return cState_partial_generate(state, string, generate_json_string); } static void State_free(void *ptr) @@ -651,7 +668,11 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) VALUE key_to_s; switch(rb_type(key)) { case T_STRING: - key_to_s = key; + if (RB_LIKELY(RBASIC_CLASS(key) == rb_cString)) { + key_to_s = key; + } else { + key_to_s = rb_funcall(key, i_to_s, 0); + } break; case T_SYMBOL: key_to_s = rb_sym2str(key); @@ -661,7 +682,11 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) break; } - generate_json_string(buffer, Vstate, state, key_to_s); + if (RB_LIKELY(RBASIC_CLASS(key_to_s) == rb_cString)) { + generate_json_string(buffer, Vstate, state, key_to_s); + } else { + generate_json(buffer, Vstate, state, key_to_s); + } if (RB_UNLIKELY(state->space_before)) fbuffer_append(buffer, state->space_before, state->space_before_len); fbuffer_append_char(buffer, ':'); if (RB_UNLIKELY(state->space)) fbuffer_append(buffer, state->space, state->space_len); @@ -826,6 +851,7 @@ static void generate_json_integer(FBuffer *buffer, VALUE Vstate, JSON_Generator_ generate_json_bignum(buffer, Vstate, state, obj); } #endif + static void generate_json_float(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj) { double value = RFLOAT_VALUE(obj); @@ -911,13 +937,14 @@ struct generate_json_data { VALUE vstate; JSON_Generator_State *state; VALUE obj; + void (*func)(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj); }; static VALUE generate_json_try(VALUE d) { struct generate_json_data *data = (struct generate_json_data *)d; - generate_json(data->buffer, data->vstate, data->state, data->obj); + data->func(data->buffer, data->vstate, data->state, data->obj); return Qnil; } @@ -927,12 +954,16 @@ static VALUE generate_json_rescue(VALUE d, VALUE exc) struct generate_json_data *data = (struct generate_json_data *)d; fbuffer_free(data->buffer); + if (RBASIC_CLASS(exc) == rb_path2class("Encoding::UndefinedConversionError")) { + exc = rb_exc_new_str(eGeneratorError, rb_funcall(exc, rb_intern("message"), 0)); + } + rb_exc_raise(exc); return Qundef; } -static VALUE cState_partial_generate(VALUE self, VALUE obj) +static VALUE cState_partial_generate(VALUE self, VALUE obj, void (*func)(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj)) { FBuffer *buffer = cState_prepare_buffer(self); GET_STATE(self); @@ -941,7 +972,8 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj) .buffer = buffer, .vstate = self, .state = state, - .obj = obj + .obj = obj, + .func = func }; rb_rescue(generate_json_try, (VALUE)&data, generate_json_rescue, (VALUE)&data); @@ -957,12 +989,18 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj) */ static VALUE cState_generate(VALUE self, VALUE obj) { - VALUE result = cState_partial_generate(self, obj); + VALUE result = cState_partial_generate(self, obj, generate_json); GET_STATE(self); (void)state; return result; } +static VALUE cState_initialize(int argc, VALUE *argv, VALUE self) +{ + rb_warn("The json gem extension was loaded with the stdlib ruby code. You should upgrade rubygems with `gem update --system`"); + return self; +} + /* * call-seq: initialize_copy(orig) * @@ -1408,6 +1446,9 @@ void Init_generator(void) cState = rb_define_class_under(mGenerator, "State", rb_cObject); rb_define_alloc_func(cState, cState_s_allocate); rb_define_singleton_method(cState, "from_state", cState_from_state_s, 1); + rb_define_method(cState, "initialize", cState_initialize, -1); + rb_define_alias(cState, "initialize", "initialize"); // avoid method redefinition warnings + rb_define_method(cState, "initialize_copy", cState_init_copy, 1); rb_define_method(cState, "indent", cState_indent, 0); rb_define_method(cState, "indent=", cState_indent_set, 1); @@ -1498,4 +1539,6 @@ void Init_generator(void) usascii_encindex = rb_usascii_encindex(); utf8_encindex = rb_utf8_encindex(); binary_encindex = rb_ascii8bit_encindex(); + + rb_require("json/ext/generator/state"); } diff --git a/ext/json/ext/generator/generator.h b/ext/json/ext/generator/generator.h index 3710ce7c..552fb8d0 100644 --- a/ext/json/ext/generator/generator.h +++ b/ext/json/ext/generator/generator.h @@ -54,17 +54,6 @@ typedef struct JSON_Generator_StateStruct { JSON_Generator_State *state; \ GET_STATE_TO(self, state) -#define GENERATE_JSON(type) \ - FBuffer *buffer; \ - VALUE Vstate; \ - JSON_Generator_State *state; \ - \ - rb_scan_args(argc, argv, "01", &Vstate); \ - Vstate = cState_from_state_s(cState, Vstate); \ - TypedData_Get_Struct(Vstate, JSON_Generator_State, &JSON_Generator_State_type, state); \ - buffer = cState_prepare_buffer(Vstate); \ - generate_json_##type(buffer, Vstate, state, self); \ - return fbuffer_to_s(buffer) static VALUE mHash_to_json(int argc, VALUE *argv, VALUE self); static VALUE mArray_to_json(int argc, VALUE *argv, VALUE self); @@ -99,7 +88,7 @@ static void generate_json_integer(FBuffer *buffer, VALUE Vstate, JSON_Generator_ static void generate_json_fixnum(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj); static void generate_json_bignum(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj); static void generate_json_float(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj); -static VALUE cState_partial_generate(VALUE self, VALUE obj); +static VALUE cState_partial_generate(VALUE self, VALUE obj, void (*func)(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *state, VALUE obj)); static VALUE cState_generate(VALUE self, VALUE obj); static VALUE cState_from_state_s(VALUE self, VALUE opts); static VALUE cState_indent(VALUE self); diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index 5a296c8f..3c8247b9 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -17,6 +17,7 @@ import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; +import org.jruby.exceptions.RaiseException; public final class Generator { private Generator() { @@ -347,7 +348,14 @@ public void visit(IRubyObject key, IRubyObject value) { } if (objectNl.length() != 0) buffer.append(indent); - STRING_HANDLER.generate(session, key.asString(), buffer); + IRubyObject keyStr = key.callMethod(context, "to_s"); + if (keyStr.getMetaClass() == runtime.getString()) { + STRING_HANDLER.generate(session, (RubyString)keyStr, buffer); + } else { + Utils.ensureString(keyStr); + Handler keyHandler = (Handler) getHandlerFor(runtime, keyStr); + keyHandler.generate(session, keyStr, buffer); + } session.infectBy(key); buffer.append(spaceBefore); @@ -383,14 +391,19 @@ void generate(Session session, RubyString object, ByteList buffer) { RuntimeInfo info = session.getInfo(); RubyString src; - if (object.encoding(session.getContext()) != info.utf8.get()) { - src = (RubyString)object.encode(session.getContext(), - info.utf8.get()); - } else { - src = object; - } + try { + if (object.encoding(session.getContext()) != info.utf8.get()) { + src = (RubyString)object.encode(session.getContext(), + info.utf8.get()); + } else { + src = object; + } - session.getStringEncoder().encode(src.getByteList(), buffer); + session.getStringEncoder().encode(src.getByteList(), buffer); + } catch (RaiseException re) { + throw Utils.newException(session.getContext(), Utils.M_GENERATOR_ERROR, + re.getMessage()); + } } }; diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 909f1a56..04ff5556 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -225,7 +225,10 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) { public IRubyObject generate(ThreadContext context, IRubyObject obj) { RubyString result = Generator.generateJson(context, obj, this); RuntimeInfo info = RuntimeInfo.forRuntime(context.getRuntime()); - result.force_encoding(context, info.utf8.get()); + if (result.encoding(context) != info.utf8.get()) { + result = (RubyString)result.dup(); + result.force_encoding(context, info.utf8.get()); + } return result; } diff --git a/lib/json/common.rb b/lib/json/common.rb index bb37820a..d79c5024 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -219,7 +219,12 @@ def parse(source, opts = nil) if opts.nil? Parser.new(source).parse else - Parser.new(source, opts).parse + # NB: The ** shouldn't be required, but we have to deal with + # different versions of the `json` and `json_pure` gems being + # loaded concurrently. + # Prior to 2.7.3, `JSON::Ext::Parser` would only take kwargs. + # Ref: https://github.com/ruby/json/issues/650 + Parser.new(source, **opts).parse end end diff --git a/lib/json/ext.rb b/lib/json/ext.rb index 775e28a9..92ef61ea 100644 --- a/lib/json/ext.rb +++ b/lib/json/ext.rb @@ -15,9 +15,6 @@ module Ext else require 'json/ext/parser' require 'json/ext/generator' - unless RUBY_ENGINE == 'jruby' - require 'json/ext/generator/state' - end $DEBUG and warn "Using Ext extension for JSON." JSON.parser = Parser JSON.generator = Generator diff --git a/lib/json/ext/generator/state.rb b/lib/json/ext/generator/state.rb index 4f9675d7..29688142 100644 --- a/lib/json/ext/generator/state.rb +++ b/lib/json/ext/generator/state.rb @@ -46,15 +46,15 @@ def configure(opts) opts.each do |key, value| case key when :indent - self.indent = value + self.indent = value || '' when :space - self.space = value + self.space = value || '' when :space_before - self.space_before = value + self.space_before = value || '' when :array_nl - self.array_nl = value + self.array_nl = value || '' when :object_nl - self.object_nl = value + self.object_nl = value || '' when :max_nesting self.max_nesting = value || 0 when :depth diff --git a/lib/json/pure/generator.rb b/lib/json/pure/generator.rb index c2268cc3..0a5c7856 100644 --- a/lib/json/pure/generator.rb +++ b/lib/json/pure/generator.rb @@ -239,13 +239,13 @@ def configure(opts) end # NOTE: If adding new instance variables here, check whether #generate should check them for #generate_json - @indent = opts[:indent] if opts.key?(:indent) - @space = opts[:space] if opts.key?(:space) - @space_before = opts[:space_before] if opts.key?(:space_before) - @object_nl = opts[:object_nl] if opts.key?(:object_nl) - @array_nl = opts[:array_nl] if opts.key?(:array_nl) - @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) - @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) + @indent = opts[:indent] || '' if opts.key?(:indent) + @space = opts[:space] || '' if opts.key?(:space) + @space_before = opts[:space_before] || '' if opts.key?(:space_before) + @object_nl = opts[:object_nl] || '' if opts.key?(:object_nl) + @array_nl = opts[:array_nl] || '' if opts.key?(:array_nl) + @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) + @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) @depth = opts[:depth] || 0 @buffer_initial_length ||= opts[:buffer_initial_length] @@ -301,19 +301,30 @@ def generate(obj) # Handles @allow_nan, @buffer_initial_length, other ivars must be the default value (see above) private def generate_json(obj, buf) - case obj - when Hash + klass = obj.class + if klass == Hash buf << '{' first = true obj.each_pair do |k,v| buf << ',' unless first - fast_serialize_string(k.to_s, buf) + + key_str = k.to_s + if key_str.is_a?(::String) + if key_str.class == ::String + fast_serialize_string(key_str, buf) + else + generate_json(key_str, buf) + end + else + raise TypeError, "#{k.class}#to_s returns an instance of #{key_str.class}, expected a String" + end + buf << ':' generate_json(v, buf) first = false end buf << '}' - when Array + elsif klass == Array buf << '[' first = true obj.each do |e| @@ -322,9 +333,9 @@ def generate(obj) first = false end buf << ']' - when String + elsif klass == String fast_serialize_string(obj, buf) - when Integer + elsif klass == Integer buf << obj.to_s else # Note: Float is handled this way since Float#to_s is slow anyway @@ -336,7 +347,11 @@ def generate(obj) if Regexp.method_defined?(:match?) private def fast_serialize_string(string, buf) # :nodoc: buf << '"' - string = string.encode(::Encoding::UTF_8) unless string.encoding == ::Encoding::UTF_8 + begin + string = string.encode(::Encoding::UTF_8) unless string.encoding == ::Encoding::UTF_8 + rescue Encoding::UndefinedConversionError => error + raise GeneratorError, error.message + end raise GeneratorError, "source sequence is illegal/malformed utf-8" unless string.valid_encoding? if /["\\\x0-\x1f]/n.match?(string) @@ -414,7 +429,15 @@ def json_transform(state) each { |key, value| result << delim unless first result << state.indent * depth if indent - result = +"#{result}#{key.to_s.to_json(state)}#{state.space_before}:#{state.space}" + + key_str = key.to_s + key_json = if key_str.is_a?(::String) + key_str = key_str.to_json(state) + else + raise TypeError, "#{key.class}#to_s returns an instance of #{key_str.class}, expected a String" + end + + result = +"#{result}#{key_json}#{state.space_before}:#{state.space}" if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value) raise GeneratorError, "#{value.class} not allowed in JSON" elsif value.respond_to?(:to_json) @@ -517,7 +540,11 @@ def to_json(state = nil, *args) end string = self else - string = encode(::Encoding::UTF_8) + begin + string = encode(::Encoding::UTF_8) + rescue Encoding::UndefinedConversionError => error + raise GeneratorError, error.message + end end if state.ascii_only? %("#{JSON.utf8_to_json_ascii(string, state.script_safe)}") diff --git a/lib/json/pure/parser.rb b/lib/json/pure/parser.rb index 3dafe830..82099e68 100644 --- a/lib/json/pure/parser.rb +++ b/lib/json/pure/parser.rb @@ -148,25 +148,25 @@ def convert_encoding(source) end # Unescape characters in strings. - UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr } - UNESCAPE_MAP.update({ - ?" => '"', - ?\\ => '\\', - ?/ => '/', - ?b => "\b", - ?f => "\f", - ?n => "\n", - ?r => "\r", - ?t => "\t", - ?u => nil, - }) + UNESCAPE_MAP = { + '"' => '"', + '\\' => '\\', + '/' => '/', + 'b' => "\b", + 'f' => "\f", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'u' => nil, + }.freeze STR_UMINUS = ''.respond_to?(:-@) def parse_string if scan(STRING) return '' if self[1].empty? - string = self[1].gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c| - if u = UNESCAPE_MAP[$&[1]] + string = self[1].gsub(%r{(?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff])}n) do |c| + k = $&[1] + if u = UNESCAPE_MAP.fetch(k) { k.chr } u else # \uXXXX bytes = ''.b diff --git a/lib/json/version.rb b/lib/json/version.rb index d3d621b7..382faf8b 100644 --- a/lib/json/version.rb +++ b/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '2.7.3' + VERSION = '2.7.6' end diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 7dc45e3a..c829e44c 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -167,6 +167,27 @@ def test_states assert s[:check_circular?] end + def test_falsy_state + object = { foo: [1, 2], bar: { egg: :spam }} + expected_json = JSON.generate( + object, + array_nl: "", + indent: "", + object_nl: "", + space: "", + space_before: "", + ) + + assert_equal expected_json, JSON.generate( + object, + array_nl: nil, + indent: nil, + object_nl: nil, + space: nil, + space_before: nil, + ) + end + def test_pretty_state state = JSON.create_pretty_state assert_equal({ @@ -261,19 +282,19 @@ def test_buffer_initial_length end def test_gc - if respond_to?(:assert_in_out_err) && !(RUBY_PLATFORM =~ /java/) - assert_in_out_err(%w[-rjson -Ilib -Iext], <<-EOS, [], []) - bignum_too_long_to_embed_as_string = 1234567890123456789012345 - expect = bignum_too_long_to_embed_as_string.to_s - GC.stress = true - - 10.times do |i| - tmp = bignum_too_long_to_embed_as_string.to_json - raise "'\#{expect}' is expected, but '\#{tmp}'" unless tmp == expect - end - EOS + pid = fork do + bignum_too_long_to_embed_as_string = 1234567890123456789012345 + expect = bignum_too_long_to_embed_as_string.to_s + GC.stress = true + + 10.times do |i| + tmp = bignum_too_long_to_embed_as_string.to_json + raise "#{expect}' is expected, but '#{tmp}'" unless tmp == expect + end end - end if GC.respond_to?(:stress=) + _, status = Process.waitpid2(pid) + assert_predicate status, :success? + end if GC.respond_to?(:stress=) && Process.respond_to?(:fork) def test_configure_using_configure_and_merge numbered_state = { @@ -449,15 +470,139 @@ def test_invalid_encoding_string end assert_includes error.message, "source sequence is illegal/malformed utf-8" - assert_raise(Encoding::UndefinedConversionError) do + assert_raise(JSON::GeneratorError) do + JSON.dump("\x82\xAC\xEF".b) + end + + assert_raise(JSON::GeneratorError) do "\x82\xAC\xEF".b.to_json end - assert_raise(Encoding::UndefinedConversionError) do - JSON.dump("\x82\xAC\xEF".b) + assert_raise(JSON::GeneratorError) do + ["\x82\xAC\xEF".b].to_json + end + + assert_raise(JSON::GeneratorError) do + { foo: "\x82\xAC\xEF".b }.to_json + end + end + + class MyCustomString < String + def to_json(_state = nil) + '"my_custom_key"' + end + + def to_s + self end end + def test_string_subclass_as_keys + # Ref: https://github.com/ruby/json/issues/667 + # if key.to_s doesn't return a bare string, we call `to_json` on it. + key = MyCustomString.new("won't be used") + assert_equal '{"my_custom_key":1}', JSON.generate(key => 1) + end + + class FakeString + def to_json(_state = nil) + raise "Shouldn't be called" + end + + def to_s + self + end + end + + def test_custom_object_as_keys + key = FakeString.new + error = assert_raise(TypeError) do + JSON.generate(key => 1) + end + assert_match "FakeString", error.message + end + + def test_to_json_called_with_state_object + object = Object.new + called = false + argument = nil + object.singleton_class.send(:define_method, :to_json) do |state| + called = true + argument = state + "" + end + + assert_equal "", JSON.dump(object) + assert called, "#to_json wasn't called" + assert_instance_of JSON::State, argument + end + + module CustomToJSON + def to_json(*) + %{"#{self.class.name}#to_json"} + end + end + + module CustomToS + def to_s + "#{self.class.name}#to_s" + end + end + + class ArrayWithToJSON < Array + include CustomToJSON + end + + def test_array_subclass_with_to_json + assert_equal '["JSONGeneratorTest::ArrayWithToJSON#to_json"]', JSON.generate([ArrayWithToJSON.new]) + assert_equal '{"[]":1}', JSON.generate(ArrayWithToJSON.new => 1) + end + + class ArrayWithToS < Array + include CustomToS + end + + def test_array_subclass_with_to_s + assert_equal '[[]]', JSON.generate([ArrayWithToS.new]) + assert_equal '{"JSONGeneratorTest::ArrayWithToS#to_s":1}', JSON.generate(ArrayWithToS.new => 1) + end + + class HashWithToJSON < Hash + include CustomToJSON + end + + def test_hash_subclass_with_to_json + assert_equal '["JSONGeneratorTest::HashWithToJSON#to_json"]', JSON.generate([HashWithToJSON.new]) + assert_equal '{"{}":1}', JSON.generate(HashWithToJSON.new => 1) + end + + class HashWithToS < Hash + include CustomToS + end + + def test_hash_subclass_with_to_s + assert_equal '[{}]', JSON.generate([HashWithToS.new]) + assert_equal '{"JSONGeneratorTest::HashWithToS#to_s":1}', JSON.generate(HashWithToS.new => 1) + end + + class StringWithToJSON < String + include CustomToJSON + end + + def test_string_subclass_with_to_json + assert_equal '["JSONGeneratorTest::StringWithToJSON#to_json"]', JSON.generate([StringWithToJSON.new]) + assert_equal '{"":1}', JSON.generate(StringWithToJSON.new => 1) + end + + class StringWithToS < String + include CustomToS + end + + def test_string_subclass_with_to_s + assert_equal '[""]', JSON.generate([StringWithToS.new]) + assert_equal '{"JSONGeneratorTest::StringWithToS#to_s":1}', JSON.generate(StringWithToS.new => 1) + end + if defined?(JSON::Ext::Generator) and RUBY_PLATFORM != "java" def test_valid_utf8_in_different_encoding utf8_string = "€™" diff --git a/test/json/ractor_test.rb b/test/json/ractor_test.rb index e0116400..f857c9a8 100644 --- a/test/json/ractor_test.rb +++ b/test/json/ractor_test.rb @@ -9,10 +9,7 @@ class JSONInRactorTest < Test::Unit::TestCase def test_generate - assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) - begin; - $VERBOSE = nil - require "json" + pid = fork do r = Ractor.new do json = JSON.generate({ 'a' => 2, @@ -26,9 +23,22 @@ def test_generate }) JSON.parse(json) end - expected_json = '{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + - '"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}' - assert_equal(JSON.parse(expected_json), r.take) - end; + expected_json = JSON.parse('{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + + '"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}') + actual_json = r.take + + if expected_json == actual_json + exit 0 + else + puts "Expected:" + puts expected_json + puts "Acutual:" + puts actual_json + puts + exit 1 + end + end + _, status = Process.waitpid2(pid) + assert_predicate status, :success? end -end if defined?(Ractor) +end if defined?(Ractor) && Process.respond_to?(:fork) diff --git a/test/json/test_helper.rb b/test/json/test_helper.rb index 4955a02c..e8bba16f 100644 --- a/test/json/test_helper.rb +++ b/test/json/test_helper.rb @@ -1,12 +1,12 @@ case ENV['JSON'] when 'pure' - $:.unshift File.join(__dir__, '../../lib') + $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__)) require 'json/pure' when 'ext' - $:.unshift File.join(__dir__, '../../ext'), File.join(__dir__, '../../lib') + $LOAD_PATH.unshift(File.expand_path('../../../ext', __FILE__), File.expand_path('../../../lib', __FILE__)) require 'json/ext' else - $:.unshift File.join(__dir__, '../../ext'), File.join(__dir__, '../../lib') + $LOAD_PATH.unshift(File.expand_path('../../../ext', __FILE__), File.expand_path('../../../lib', __FILE__)) require 'json' end