diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 8930213b..af783f9c 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -926,11 +926,6 @@ static size_t State_memsize(const void *ptr) return sizeof(JSON_Generator_State); } -#ifndef HAVE_RB_EXT_RACTOR_SAFE -# undef RUBY_TYPED_FROZEN_SHAREABLE -# define RUBY_TYPED_FROZEN_SHAREABLE 0 -#endif - static const rb_data_type_t JSON_Generator_State_type = { "JSON/Generator/State", { @@ -1630,6 +1625,7 @@ static VALUE string_config(VALUE config) */ static VALUE cState_indent_set(VALUE self, VALUE indent) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->indent, string_config(indent)); return Qnil; @@ -1655,6 +1651,7 @@ static VALUE cState_space(VALUE self) */ static VALUE cState_space_set(VALUE self, VALUE space) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->space, string_config(space)); return Qnil; @@ -1678,6 +1675,7 @@ static VALUE cState_space_before(VALUE self) */ static VALUE cState_space_before_set(VALUE self, VALUE space_before) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->space_before, string_config(space_before)); return Qnil; @@ -1703,6 +1701,7 @@ static VALUE cState_object_nl(VALUE self) */ static VALUE cState_object_nl_set(VALUE self, VALUE object_nl) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->object_nl, string_config(object_nl)); return Qnil; @@ -1726,6 +1725,7 @@ static VALUE cState_array_nl(VALUE self) */ static VALUE cState_array_nl_set(VALUE self, VALUE array_nl) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->array_nl, string_config(array_nl)); return Qnil; @@ -1749,6 +1749,7 @@ static VALUE cState_as_json(VALUE self) */ static VALUE cState_as_json_set(VALUE self, VALUE as_json) { + rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->as_json, rb_convert_type(as_json, T_DATA, "Proc", "to_proc")); return Qnil; @@ -1791,6 +1792,7 @@ static long long_config(VALUE num) */ static VALUE cState_max_nesting_set(VALUE self, VALUE depth) { + rb_check_frozen(self); GET_STATE(self); state->max_nesting = long_config(depth); return Qnil; @@ -1816,6 +1818,7 @@ static VALUE cState_script_safe(VALUE self) */ static VALUE cState_script_safe_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->script_safe = RTEST(enable); return Qnil; @@ -1847,6 +1850,7 @@ static VALUE cState_strict(VALUE self) */ static VALUE cState_strict_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->strict = RTEST(enable); return Qnil; @@ -1871,6 +1875,7 @@ static VALUE cState_allow_nan_p(VALUE self) */ static VALUE cState_allow_nan_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->allow_nan = RTEST(enable); return Qnil; @@ -1895,6 +1900,7 @@ static VALUE cState_ascii_only_p(VALUE self) */ static VALUE cState_ascii_only_set(VALUE self, VALUE enable) { + rb_check_frozen(self); GET_STATE(self); state->ascii_only = RTEST(enable); return Qnil; @@ -1932,6 +1938,7 @@ static VALUE cState_depth(VALUE self) */ static VALUE cState_depth_set(VALUE self, VALUE depth) { + rb_check_frozen(self); GET_STATE(self); state->depth = long_config(depth); return Qnil; @@ -1965,6 +1972,7 @@ static void buffer_initial_length_set(JSON_Generator_State *state, VALUE buffer_ */ static VALUE cState_buffer_initial_length_set(VALUE self, VALUE buffer_initial_length) { + rb_check_frozen(self); GET_STATE(self); buffer_initial_length_set(state, buffer_initial_length); return Qnil; @@ -2031,6 +2039,7 @@ static void configure_state(JSON_Generator_State *state, VALUE vstate, VALUE con static VALUE cState_configure(VALUE self, VALUE opts) { + rb_check_frozen(self); GET_STATE(self); configure_state(state, self, opts); return self; diff --git a/ext/json/ext/json.h b/ext/json/ext/json.h index 01fe0cd0..28efa04c 100644 --- a/ext/json/ext/json.h +++ b/ext/json/ext/json.h @@ -45,6 +45,11 @@ typedef unsigned char _Bool; #endif #endif +#ifndef HAVE_RB_EXT_RACTOR_SAFE +# undef RUBY_TYPED_FROZEN_SHAREABLE +# define RUBY_TYPED_FROZEN_SHAREABLE 0 +#endif + #ifndef NORETURN #define NORETURN(x) x #endif diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index 62ee1f24..fb89a88d 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -1467,6 +1467,7 @@ static void parser_config_init(JSON_ParserConfig *config, VALUE opts) */ static VALUE cParserConfig_initialize(VALUE self, VALUE opts) { + rb_check_frozen(self); GET_PARSER_CONFIG; parser_config_init(config, opts); @@ -1562,7 +1563,7 @@ static const rb_data_type_t JSON_ParserConfig_type = { JSON_ParserConfig_memsize, }, 0, 0, - RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, + RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE, }; static VALUE cJSON_parser_s_allocate(VALUE klass) diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index fd1478b0..19d6a134 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -283,6 +283,7 @@ public IRubyObject op_aref(ThreadContext context, IRubyObject vName) { @JRubyMethod(name="[]=") public IRubyObject op_aset(ThreadContext context, IRubyObject vName, IRubyObject value) { + checkFrozen(); String name = vName.asJavaString(); String nameWriter = name + "="; if (getMetaClass().isMethodBound(nameWriter, true)) { @@ -304,6 +305,7 @@ public RubyString indent_get(ThreadContext context) { @JRubyMethod(name="indent=") public IRubyObject indent_set(ThreadContext context, IRubyObject indent) { + checkFrozen(); this.indent = prepareByteList(context, indent); return indent; } @@ -319,6 +321,7 @@ public RubyString space_get(ThreadContext context) { @JRubyMethod(name="space=") public IRubyObject space_set(ThreadContext context, IRubyObject space) { + checkFrozen(); this.space = prepareByteList(context, space); return space; } @@ -335,6 +338,7 @@ public RubyString space_before_get(ThreadContext context) { @JRubyMethod(name="space_before=") public IRubyObject space_before_set(ThreadContext context, IRubyObject spaceBefore) { + checkFrozen(); this.spaceBefore = prepareByteList(context, spaceBefore); return spaceBefore; } @@ -351,6 +355,7 @@ public RubyString object_nl_get(ThreadContext context) { @JRubyMethod(name="object_nl=") public IRubyObject object_nl_set(ThreadContext context, IRubyObject objectNl) { + checkFrozen(); this.objectNl = prepareByteList(context, objectNl); return objectNl; } @@ -367,6 +372,7 @@ public RubyString array_nl_get(ThreadContext context) { @JRubyMethod(name="array_nl=") public IRubyObject array_nl_set(ThreadContext context, IRubyObject arrayNl) { + checkFrozen(); this.arrayNl = prepareByteList(context, arrayNl); return arrayNl; } @@ -382,6 +388,7 @@ public IRubyObject as_json_get(ThreadContext context) { @JRubyMethod(name="as_json=") public IRubyObject as_json_set(ThreadContext context, IRubyObject asJSON) { + checkFrozen(); if (asJSON.isNil() || asJSON == context.getRuntime().getFalse()) { this.asJSON = null; } else { @@ -402,6 +409,7 @@ public RubyInteger max_nesting_get(ThreadContext context) { @JRubyMethod(name="max_nesting=") public IRubyObject max_nesting_set(IRubyObject max_nesting) { + checkFrozen(); maxNesting = RubyNumeric.fix2int(max_nesting); return max_nesting; } @@ -420,6 +428,7 @@ public RubyBoolean script_safe_get(ThreadContext context) { @JRubyMethod(name="script_safe=", alias="escape_slash=") public IRubyObject script_safe_set(IRubyObject script_safe) { + checkFrozen(); scriptSafe = script_safe.isTrue(); return script_safe.getRuntime().newBoolean(scriptSafe); } @@ -443,6 +452,7 @@ public RubyBoolean strict_get(ThreadContext context) { @JRubyMethod(name="strict=") public IRubyObject strict_set(IRubyObject isStrict) { + checkFrozen(); strict = isStrict.isTrue(); return isStrict.getRuntime().newBoolean(strict); } @@ -472,6 +482,7 @@ public RubyInteger buffer_initial_length_get(ThreadContext context) { @JRubyMethod(name="buffer_initial_length=") public IRubyObject buffer_initial_length_set(IRubyObject buffer_initial_length) { + checkFrozen(); int newLength = RubyNumeric.fix2int(buffer_initial_length); if (newLength > 0) bufferInitialLength = newLength; return buffer_initial_length; @@ -488,6 +499,7 @@ public RubyInteger depth_get(ThreadContext context) { @JRubyMethod(name="depth=") public IRubyObject depth_set(IRubyObject vDepth) { + checkFrozen(); depth = RubyNumeric.fix2int(vDepth); return vDepth; } @@ -532,6 +544,7 @@ public boolean getDeprecateDuplicateKey() { */ @JRubyMethod(visibility=Visibility.PRIVATE) public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) { + checkFrozen(); OptionsReader opts = new OptionsReader(context, vOpts); ByteList indent = opts.getString("indent"); diff --git a/java/src/json/ext/ParserConfig.java b/java/src/json/ext/ParserConfig.java index 2bd03bab..02bd7e5e 100644 --- a/java/src/json/ext/ParserConfig.java +++ b/java/src/json/ext/ParserConfig.java @@ -172,6 +172,7 @@ public static IRubyObject newInstance(IRubyObject clazz, IRubyObject arg0, IRuby @JRubyMethod(visibility = Visibility.PRIVATE) public IRubyObject initialize(ThreadContext context, IRubyObject options) { + checkFrozen(); Ruby runtime = context.runtime; OptionsReader opts = new OptionsReader(context, options); diff --git a/lib/json/common.rb b/lib/json/common.rb index f22d911f..233b8c7e 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -1048,7 +1048,7 @@ def initialize(options = nil, &as_json) options[:as_json] = as_json if as_json @state = State.new(options).freeze - @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options)) + @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options)).freeze end # call-seq: diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 1b3c702c..54a2ec61 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -901,4 +901,18 @@ def test_generate_duplicate_keys_disallowed end assert_equal %(detected duplicate key "foo" in #{hash.inspect}), error.message end + + def test_frozen + state = JSON::State.new.freeze + assert_raise(FrozenError) do + state.configure(max_nesting: 1) + end + setters = state.methods.grep(/\w=$/) + assert_not_empty setters + setters.each do |setter| + assert_raise(FrozenError) do + state.send(setter, 1) + end + end + end end diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 6315c3e6..a5b47636 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -820,6 +820,14 @@ def test_parse_whitespace_after_newline assert_equal [], JSON.parse("[\n#{' ' * (8 + 8 + 4 + 3)}]") end + def test_frozen + parser_config = JSON::Parser::Config.new({}).freeze + omit "JRuby failure in CI" if RUBY_ENGINE == "jruby" + assert_raise FrozenError do + parser_config.send(:initialize, {}) + end + end + private def assert_equal_float(expected, actual, delta = 1e-2) diff --git a/test/json/ractor_test.rb b/test/json/ractor_test.rb index 53e1099c..e53c405a 100644 --- a/test/json/ractor_test.rb +++ b/test/json/ractor_test.rb @@ -52,4 +52,63 @@ def test_generate _, status = Process.waitpid2(pid) assert_predicate status, :success? end + + def test_coder + coder = JSON::Coder.new.freeze + assert Ractor.shareable?(coder) + pid = fork do + Warning[:experimental] = false + r = Ractor.new(coder) do |coder| + json = coder.dump({ + 'a' => 2, + 'b' => 3.141, + 'c' => 'c', + 'd' => [ 1, "b", 3.14 ], + 'e' => { 'foo' => 'bar' }, + 'g' => "\"\0\037", + 'h' => 1000.0, + 'i' => 0.001 + }) + coder.load(json) + 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.value + + if expected_json == actual_json + exit 0 + else + puts "Expected:" + puts expected_json + puts "Actual:" + puts actual_json + puts + exit 1 + end + end + _, status = Process.waitpid2(pid) + assert_predicate status, :success? + end + + class NonNative + def initialize(value) + @value = value + end + end + + def test_coder_proc + block = Ractor.shareable_proc { |value| value.as_json } + coder = JSON::Coder.new(&block).freeze + assert Ractor.shareable?(coder) + + pid = fork do + Warning[:experimental] = false + assert_equal [{}], Ractor.new(coder) { |coder| + coder.load('[{}]') + }.value + end + + _, status = Process.waitpid2(pid) + assert_predicate status, :success? + end if Ractor.respond_to?(:shareable_proc) end if defined?(Ractor) && Process.respond_to?(:fork)