From 8a9860748f30777d39ce1916365714d2282201be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Sun, 23 Nov 2025 11:36:58 +0100 Subject: [PATCH 1/6] Test that depth of unfrozen State does not change --- test/json/json_generator_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 54a2ec61..f1fb72ee 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -915,4 +915,13 @@ def test_frozen end end end + + # The case when the State is frozen is tested in JSONCoderTest#test_nesting_recovery + def test_nesting_recovery + state = JSON::State.new + ary = [] + ary << ary + assert_raise(JSON::NestingError) { state.generate_new(ary) } + assert_equal '{"a":1}', state.generate({ a: 1 }) + end end From c9b11ce0b6c05652bcb10a32648b89c448e3e8a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Mon, 24 Nov 2025 15:40:06 +0100 Subject: [PATCH 2/6] Test depth --- test/json/json_generator_test.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index f1fb72ee..0931668f 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -282,6 +282,16 @@ def test_allow_nan end def test_depth + pretty = { object_nl: "\n", array_nl: "\n", space: " ", indent: " " } + state = JSON.state.new(**pretty) + assert_equal %({\n "foo": 42\n}), JSON.generate({ foo: 42 }, pretty) + assert_equal %({\n "foo": 42\n}), state.generate(foo: 42) + state.depth = 1 + assert_equal %({\n "foo": 42\n }), JSON.generate({ foo: 42 }, pretty.merge(depth: 1)) + assert_equal %({\n "foo": 42\n }), state.generate(foo: 42) + end + + def test_depth_nesting_error ary = []; ary << ary assert_raise(JSON::NestingError) { generate(ary) } assert_raise(JSON::NestingError) { JSON.pretty_generate(ary) } From e33fbd36c54ebb2e049c496b9f312e91a54dff8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Wed, 26 Nov 2025 11:03:33 +0100 Subject: [PATCH 3/6] Test to_json using State#depth --- test/json/json_generator_test.rb | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 0931668f..c01ed678 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -92,6 +92,46 @@ def test_dump_strict assert_equal '"World"', "World".to_json(strict: true) end + def test_state_depth_to_json + depth = Object.new + def depth.to_json(state) + JSON::State.from_state(state).depth.to_s + end + + assert_equal "0", JSON.generate(depth) + assert_equal "[1]", JSON.generate([depth]) + assert_equal %({"depth":1}), JSON.generate(depth: depth) + assert_equal "[[2]]", JSON.generate([[depth]]) + assert_equal %([{"depth":2}]), JSON.generate([{depth: depth}]) + + state = JSON::State.new + assert_equal "0", state.generate(depth) + assert_equal "[1]", state.generate([depth]) + assert_equal %({"depth":1}), state.generate(depth: depth) + assert_equal "[[2]]", state.generate([[depth]]) + assert_equal %([{"depth":2}]), state.generate([{depth: depth}]) + end + + def test_state_depth_to_json_recursive + recur = Object.new + def recur.to_json(state = nil, *) + state = JSON::State.from_state(state) + if state.depth < 3 + state.generate([state.depth, self]) + else + state.generate([state.depth]) + end + end + + assert_raise(NestingError) { JSON.generate(recur, max_nesting: 3) } + assert_equal "[0,[1,[2,[3]]]]", JSON.generate(recur, max_nesting: 4) + + state = JSON::State.new(max_nesting: 3) + assert_raise(NestingError) { state.generate(recur) } + state.max_nesting = 4 + assert_equal "[0,[1,[2,[3]]]]", JSON.generate(recur, max_nesting: 4) + end + def test_generate_pretty json = pretty_generate({}) assert_equal('{}', json) From c259720adbdef95b15c492a27e95127b4dcce983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Sat, 22 Nov 2025 12:30:44 +0100 Subject: [PATCH 4/6] Add depth to struct generate_json_data Instead of incrementing JSON_Generator_State::depth, we now increment generate_json_data::depth, and only copied at the end. --- ext/json/ext/generator/generator.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 8d04bef5..7125f443 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -60,6 +60,7 @@ struct generate_json_data { JSON_Generator_State *state; VALUE obj; generator_func func; + long depth; }; static VALUE cState_from_state_s(VALUE self, VALUE opts); @@ -972,6 +973,8 @@ static inline VALUE vstate_get(struct generate_json_data *data) if (RB_UNLIKELY(!data->vstate)) { vstate_spill(data); } + GET_STATE(data->vstate); + state->depth = data->depth; return data->vstate; } @@ -1145,7 +1148,7 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) FBuffer *buffer = data->buffer; JSON_Generator_State *state = data->state; - long depth = state->depth; + long depth = data->depth; int key_type = rb_type(key); if (arg->first) { @@ -1219,9 +1222,9 @@ json_object_i(VALUE key, VALUE val, VALUE _arg) static inline long increase_depth(struct generate_json_data *data) { JSON_Generator_State *state = data->state; - long depth = ++state->depth; + long depth = ++data->depth; if (RB_UNLIKELY(depth > state->max_nesting && state->max_nesting)) { - rb_raise(eNestingError, "nesting of %ld is too deep. Did you try to serialize objects with circular references?", --state->depth); + rb_raise(eNestingError, "nesting of %ld is too deep. Did you try to serialize objects with circular references?", --data->depth); } return depth; } @@ -1232,7 +1235,7 @@ static void generate_json_object(FBuffer *buffer, struct generate_json_data *dat if (RHASH_SIZE(obj) == 0) { fbuffer_append(buffer, "{}", 2); - --data->state->depth; + --data->depth; return; } @@ -1245,7 +1248,7 @@ static void generate_json_object(FBuffer *buffer, struct generate_json_data *dat }; rb_hash_foreach(obj, json_object_i, (VALUE)&arg); - depth = --data->state->depth; + depth = --data->depth; if (RB_UNLIKELY(data->state->object_nl)) { fbuffer_append_str(buffer, data->state->object_nl); if (RB_UNLIKELY(data->state->indent)) { @@ -1261,7 +1264,7 @@ static void generate_json_array(FBuffer *buffer, struct generate_json_data *data if (RARRAY_LEN(obj) == 0) { fbuffer_append(buffer, "[]", 2); - --data->state->depth; + --data->depth; return; } @@ -1277,7 +1280,7 @@ static void generate_json_array(FBuffer *buffer, struct generate_json_data *data } generate_json(buffer, data, RARRAY_AREF(obj, i)); } - data->state->depth = --depth; + data->depth = --depth; if (RB_UNLIKELY(data->state->array_nl)) { fbuffer_append_str(buffer, data->state->array_nl); if (RB_UNLIKELY(data->state->indent)) { @@ -1358,7 +1361,7 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data if (casted_obj != obj) { increase_depth(data); generate_json(buffer, data, casted_obj); - data->state->depth--; + data->depth--; return; } } @@ -1477,6 +1480,7 @@ static VALUE generate_json_ensure(VALUE d) { struct generate_json_data *data = (struct generate_json_data *)d; fbuffer_free(data->buffer); + data->state->depth = data->depth; return Qundef; } @@ -1495,6 +1499,7 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func, .buffer = &buffer, .vstate = self, .state = state, + .depth = state->depth, .obj = obj, .func = func }; @@ -1541,6 +1546,7 @@ static VALUE cState_generate_new(int argc, VALUE *argv, VALUE self) .buffer = &buffer, .vstate = Qfalse, .state = &new_state, + .depth = new_state.depth, .obj = obj, .func = generate_json }; @@ -2061,6 +2067,7 @@ static VALUE cState_m_generate(VALUE klass, VALUE obj, VALUE opts, VALUE io) .buffer = &buffer, .vstate = Qfalse, .state = &state, + .depth = state.depth, .obj = obj, .func = generate_json, }; From 496ec8b6336c6be7d4b52c19554c2cb87c907bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Sat, 22 Nov 2025 13:59:16 +0100 Subject: [PATCH 5/6] Don't write depth to JSON_Generator_State in some cases For `JSON.generate` and `JSON::State#generate_new`, don't copy generate_json_data::depth to JSON_Generator_State::depth. In `JSON.generate`, the JSON_Generator_State is on the stack and discarded anyway. In `JSON::State#generate_new`, we copy the struct to avoid mutating the original one. --- ext/json/ext/generator/generator.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 7125f443..8f908062 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1476,7 +1476,8 @@ static VALUE generate_json_try(VALUE d) return fbuffer_finalize(data->buffer); } -static VALUE generate_json_ensure(VALUE d) +// 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); @@ -1485,6 +1486,14 @@ static VALUE generate_json_ensure(VALUE d) return Qundef; } +static VALUE generate_json_ensure(VALUE d) +{ + struct generate_json_data *data = (struct generate_json_data *)d; + fbuffer_free(data->buffer); + + return Qundef; +} + static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func, VALUE io) { GET_STATE(self); @@ -1503,7 +1512,7 @@ static VALUE cState_partial_generate(VALUE self, VALUE obj, generator_func func, .obj = obj, .func = func }; - return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure, (VALUE)&data); + return rb_ensure(generate_json_try, (VALUE)&data, generate_json_ensure_deprecated, (VALUE)&data); } /* call-seq: From 95f8bc78a0367f445f4324b41aabb4a6a1a5b9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Sat, 22 Nov 2025 14:57:30 +0100 Subject: [PATCH 6/6] Don't copy JSON_Generator_State in generate_new Now that the state isn't mutated in generate_new, we no longer need to copy the struct, we can just use it. --- ext/json/ext/generator/generator.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 8f908062..32a9b485 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1539,12 +1539,6 @@ static VALUE cState_generate_new(int argc, VALUE *argv, VALUE self) GET_STATE(self); - JSON_Generator_State new_state; - MEMCPY(&new_state, state, JSON_Generator_State, 1); - - // FIXME: depth shouldn't be part of JSON_Generator_State, as that prevents it from being used concurrently. - new_state.depth = 0; - char stack_buffer[FBUFFER_STACK_SIZE]; FBuffer buffer = { .io = RTEST(io) ? io : Qfalse, @@ -1554,8 +1548,8 @@ static VALUE cState_generate_new(int argc, VALUE *argv, VALUE self) struct generate_json_data data = { .buffer = &buffer, .vstate = Qfalse, - .state = &new_state, - .depth = new_state.depth, + .state = state, + .depth = 0, .obj = obj, .func = generate_json };