diff --git a/CHANGES.md b/CHANGES.md index a5728aba..06e56aed 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,12 @@ * Improve `JSON.load` and `JSON.unsafe_load` to allow passing options as second argument. * Fix the parser to no longer ignore invalid escapes in strings. Only `\"`, `\\`, `\b`, `\f`, `\n`, `\r`, `\t` and `\u` are valid JSON escapes. +* Fixed `JSON::Coder` to use the depth it was initialized with. * On TruffleRuby, fix the generator to not call `to_json` on the return value of `as_json` for `Float::NAN`. +* Fixed handling of `state.depth`: when `to_json` changes `state.depth` but does not restore it, it is reset + automatically to its initial value. + In particular, when a `NestingError` is raised, `depth` is no longer equal to `max_nesting` after the call to + generate, and is reset to its initial value. Similarly when `to_json` raises an exception. ### 2025-11-07 (2.16.0) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 9e6e617a..d202e97e 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -968,14 +968,16 @@ static void vstate_spill(struct generate_json_data *data) RB_OBJ_WRITTEN(vstate, Qundef, state->as_json); } -static inline VALUE vstate_get(struct generate_json_data *data) +static inline VALUE json_call_to_json(struct generate_json_data *data, VALUE obj) { if (RB_UNLIKELY(!data->vstate)) { vstate_spill(data); } GET_STATE(data->vstate); state->depth = data->depth; - return data->vstate; + VALUE tmp = rb_funcall(obj, i_to_json, 1, data->vstate); + // no need to restore state->depth, vstate is just a temporary State + return tmp; } static VALUE @@ -1293,9 +1295,7 @@ static void generate_json_fallback(FBuffer *buffer, struct generate_json_data *d { VALUE tmp; if (rb_respond_to(obj, i_to_json)) { - tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data)); - GET_STATE(data->vstate); - data->depth = state->depth; + tmp = json_call_to_json(data, obj); Check_Type(tmp, T_STRING); fbuffer_append_str(buffer, tmp); } else { @@ -1477,16 +1477,6 @@ static VALUE generate_json_try(VALUE d) return fbuffer_finalize(data->buffer); } -// Preserves the deprecated behavior of State#depth being set. -static VALUE generate_json_ensure_deprecated(VALUE d) -{ - struct generate_json_data *data = (struct generate_json_data *)d; - fbuffer_free(data->buffer); - data->state->depth = data->depth; - - return Qundef; -} - static VALUE generate_json_ensure(VALUE d) { struct generate_json_data *data = (struct generate_json_data *)d; @@ -1507,13 +1497,13 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func, struct generate_json_data data = { .buffer = &buffer, - .vstate = self, + .vstate = Qfalse, // don't use self as it may be frozen and its depth is mutated when calling to_json .state = state, .depth = state->depth, .obj = obj, .func = func }; - return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure_deprecated, (VALUE)&data); + return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure, (VALUE)&data); } /* call-seq: @@ -1532,31 +1522,6 @@ static VALUE cState_generate(int argc, VALUE *argv, VALUE self) return cState_partial_generate(self, obj, generate_json, io); } -static VALUE cState_generate_new(int argc, VALUE *argv, VALUE self) -{ - rb_check_arity(argc, 1, 2); - VALUE obj = argv[0]; - VALUE io = argc > 1 ? argv[1] : Qnil; - - GET_STATE(self); - - char stack_buffer[FBUFFER_STACK_SIZE]; - FBuffer buffer = { - .io = RTEST(io) ? io : Qfalse, - }; - fbuffer_stack_init(&buffer, state->buffer_initial_length, stack_buffer, FBUFFER_STACK_SIZE); - - struct generate_json_data data = { - .buffer = &buffer, - .vstate = Qfalse, - .state = state, - .depth = state->depth, - .obj = obj, - .func = generate_json - }; - return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure, (VALUE)&data); -} - 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`"); @@ -2145,7 +2110,6 @@ void Init_generator(void) rb_define_method(cState, "buffer_initial_length", cState_buffer_initial_length, 0); rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1); rb_define_method(cState, "generate", cState_generate, -1); - rb_define_method(cState, "generate_new", cState_generate_new, -1); // :nodoc: rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0); diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index 0caa6948..ea9e6d08 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -54,7 +54,13 @@ static RubyString generateJson(ThreadContext context, T static RubyString generateJson(ThreadContext context, T object, Handler handler, IRubyObject arg0) { Session session = new Session(arg0); - return handler.generateNew(context, session, object); + GeneratorState state = session.getState(context); + int depth = state.depth; + try { + return handler.generateNew(context, session, object); + } finally { + state.depth = depth; + } } /** @@ -79,18 +85,25 @@ static RubyString generateJson(ThreadContext context, T generateJson(ThreadContext context, T object, GeneratorState config, IRubyObject io) { Session session = new Session(config); - Handler handler = getHandlerFor(context.runtime, object); + GeneratorState state = session.getState(context); + int depth = state.depth; - if (io.isNil()) { - return handler.generateNew(context, session, object); - } + try { + Handler handler = getHandlerFor(context.runtime, object); + + if (io.isNil()) { + return handler.generateNew(context, session, object); + } - BufferedOutputStream buffer = - new BufferedOutputStream( - new PatchedIOOutputStream(io, UTF8Encoding.INSTANCE), - IO_BUFFER_SIZE); - handler.generateToBuffer(context, session, object, buffer); - return io; + BufferedOutputStream buffer = + new BufferedOutputStream( + new PatchedIOOutputStream(io, UTF8Encoding.INSTANCE), + IO_BUFFER_SIZE); + handler.generateToBuffer(context, session, object, buffer); + return io; + } finally { + state.depth = depth; + } } /** @@ -784,7 +797,13 @@ static RubyString generateGenericNew(ThreadContext context, Session session, IRu } throw Utils.buildGeneratorError(context, object, object + " not allowed in JSON").toThrowable(); } else if (object.respondsTo("to_json")) { - IRubyObject result = object.callMethod(context, "to_json", state); + int depth = state.depth; + IRubyObject result; + try { + result = object.callMethod(context, "to_json", state); + } finally { + state.depth = depth; + } if (result instanceof RubyString) return (RubyString)result; throw context.runtime.newTypeError("to_json must return a String"); } else { diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 327405a8..8db31bda 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -256,18 +256,6 @@ public IRubyObject generate(ThreadContext context, IRubyObject obj) { return generate(context, obj, context.nil); } - @JRubyMethod - public IRubyObject generate_new(ThreadContext context, IRubyObject obj, IRubyObject io) { - GeneratorState newState = (GeneratorState)dup(); - return newState.generate(context, obj, io); - } - - @JRubyMethod - public IRubyObject generate_new(ThreadContext context, IRubyObject obj) { - GeneratorState newState = (GeneratorState)dup(); - return newState.generate(context, obj, context.nil); - } - @JRubyMethod(name="[]") public IRubyObject op_aref(ThreadContext context, IRubyObject vName) { String name = vName.asJavaString(); diff --git a/lib/json/common.rb b/lib/json/common.rb index fcdc0d9f..877b9681 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -1074,7 +1074,7 @@ def initialize(options = nil, &as_json) # # Serialize the given object into a \JSON document. def dump(object, io = nil) - @state.generate_new(object, io) + @state.generate(object, io) end alias_method :generate, :dump diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 7b9657a3..d7118884 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -330,6 +330,9 @@ def to_h # created this method raises a # GeneratorError exception. def generate(obj, anIO = nil) + return dup.generate(obj, anIO) if frozen? + + depth = @depth if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and !@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj) result = generate_json(obj, ''.dup) @@ -346,10 +349,8 @@ def generate(obj, anIO = nil) else result end - end - - def generate_new(obj, anIO = nil) # :nodoc: - dup.generate(obj, anIO) + ensure + @depth = depth unless frozen? end # Handles @allow_nan, @buffer_initial_length, other ivars must be the default value (see above) @@ -490,8 +491,11 @@ module Hash # _depth_ is used to find out nesting depth, to indent accordingly. def to_json(state = nil, *) state = State.from_state(state) + depth = state.depth state.check_max_nesting json_transform(state) + ensure + state.depth = depth end private @@ -555,17 +559,19 @@ def json_transform(state) raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end result << value.to_json(state) + state.depth = depth else raise GeneratorError.new("#{value.class} not allowed in JSON", value) end elsif value.respond_to?(:to_json) result << value.to_json(state) + state.depth = depth else result << %{"#{String(value)}"} end first = false } - depth = state.depth -= 1 + depth -= 1 unless first result << state.object_nl result << state.indent * depth if indent @@ -582,8 +588,11 @@ module Array # produced JSON string output further. def to_json(state = nil, *) state = State.from_state(state) + depth = state.depth state.check_max_nesting json_transform(state) + ensure + state.depth = depth end private @@ -621,12 +630,13 @@ def json_transform(state) end elsif value.respond_to?(:to_json) result << value.to_json(state) + state.depth = depth else result << %{"#{String(value)}"} end first = false } - state.depth = depth -= 1 + depth -= 1 result << state.array_nl result << state.indent * depth if indent result << ']' diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index e623e054..9f8b35de 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -321,7 +321,8 @@ def test_allow_nan end end - def test_depth_bad_to_json + # An object that changes state.depth when it receives to_json(state) + def bad_to_json obj = Object.new def obj.to_json(state) state.depth += 1 @@ -329,21 +330,44 @@ def obj.to_json(state) "#{state.indent * state.depth}\"foo\":#{state.space}1#{state.object_nl}"\ "#{state.indent * (state.depth - 1)}}" end - assert_equal <<~JSON.chomp, JSON.pretty_generate([obj] * 2) + obj + end + + def test_depth_restored_bad_to_json + state = JSON::State.new + state.generate(bad_to_json) + assert_equal 0, state.depth + end + + def test_depth_restored_bad_to_json_in_Array + assert_equal <<~JSON.chomp, JSON.pretty_generate([bad_to_json] * 2) [ { "foo": 1 }, { - "foo": 1 - } + "foo": 1 + } ] JSON - state = JSON::State.new(object_nl: "\n", array_nl: "\n", space: " ", indent: " ") - state.generate(obj) - assert_equal 1, state.depth # FIXME - state.depth = 0 - state.generate([obj]) + state = JSON::State.new + state.generate([bad_to_json]) + assert_equal 0, state.depth + end + + def test_depth_restored_bad_to_json_in_Hash + assert_equal <<~JSON.chomp, JSON.pretty_generate(a: bad_to_json, b: bad_to_json) + { + "a": { + "foo": 1 + }, + "b": { + "foo": 1 + } + } + JSON + state = JSON::State.new + state.generate(a: bad_to_json) assert_equal 0, state.depth end @@ -361,10 +385,36 @@ def test_depth_nesting_error ary = []; ary << ary assert_raise(JSON::NestingError) { generate(ary) } assert_raise(JSON::NestingError) { JSON.pretty_generate(ary) } - s = JSON.state.new - assert_equal 0, s.depth + end + + def test_depth_nesting_error_to_json + ary = []; ary << ary + s = JSON.state.new(depth: 1) assert_raise(JSON::NestingError) { ary.to_json(s) } - assert_equal 100, s.depth + assert_equal 1, s.depth + end + + def test_depth_nesting_error_Hash_to_json + hash = {}; hash[:a] = hash + s = JSON.state.new(depth: 1) + assert_raise(JSON::NestingError) { hash.to_json(s) } + assert_equal 1, s.depth + end + + def test_depth_nesting_error_generate + ary = []; ary << ary + s = JSON.state.new(depth: 1) + assert_raise(JSON::NestingError) { s.generate(ary) } + assert_equal 1, s.depth + end + + def test_depth_exception_calling_to_json + def (obj = Object.new).to_json(*) + raise + end + s = JSON.state.new(depth: 1).freeze + assert_raise(RuntimeError) { s.generate([{ hash: obj }]) } + assert_equal 1, s.depth end def test_buffer_initial_length @@ -1006,7 +1056,8 @@ def test_nesting_recovery state = JSON::State.new ary = [] ary << ary - assert_raise(JSON::NestingError) { state.generate_new(ary) } + assert_raise(JSON::NestingError) { state.generate(ary) } + assert_equal 0, state.depth assert_equal '{"a":1}', state.generate({ a: 1 }) end end