diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 32f38a58..9af35a08 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -1307,10 +1307,28 @@ static void generate_json_no_fallback(FBuffer *buffer, struct generate_json_data generate_json_general(buffer, data, obj, false); } +static void validate_rfc8785(JSON_Generator_State *state) +{ + if (!state->rfc8785) return; + + const char *option = state->indent ? "indent" : + state->space ? "space" : + state->space_before ? "space_before" : + state->object_nl ? "object_nl" : + state->array_nl ? "array_nl" : + state->ascii_only ? "ascii_only" : + state->script_safe ? "script_safe" : + state->allow_nan ? "allow_nan" : NULL; + if (option) { + rb_raise(rb_eArgError, "%s cannot be used with rfc8785", option); + } +} + static VALUE generate_json_try(VALUE d) { struct generate_json_data *data = (struct generate_json_data *)d; + validate_rfc8785(data->state); data->func(data->buffer, data, data->obj); return fbuffer_finalize(data->buffer); @@ -1990,6 +2008,7 @@ static void configure_state(JSON_Generator_State *state, VALUE vstate, VALUE con rb_hash_foreach(config, configure_state_i, (VALUE)&data); raise_argument_error_on_unknown_keywords(data.unknown_keywords); + validate_rfc8785(state); } static VALUE cState_configure(VALUE self, VALUE opts) diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index f191993d..d653e582 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -56,6 +56,7 @@ static RubyString generateJson(ThreadContext context, T static RubyString generateJson(ThreadContext context, T object, Handler handler, IRubyObject arg0) { Session session = new Session(arg0); GeneratorState state = session.getState(context); + state.validateRfc8785(context); int depth = state.depth; try { return handler.generateNew(context, session, object); @@ -87,6 +88,7 @@ static RubyString generateJson(ThreadContext context, T GeneratorState config, IRubyObject io) { Session session = new Session(config); GeneratorState state = session.getState(context); + state.validateRfc8785(context); int depth = state.depth; try { diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index d702ae8a..a0f3dfc2 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -525,6 +525,22 @@ public boolean rfc8785() { return this.rfc8785; } + public void validateRfc8785(ThreadContext context) { + if (!rfc8785) return; + + String option = !indent.isEmpty() ? "indent" : + !space.isEmpty() ? "space" : + !spaceBefore.isEmpty() ? "space_before" : + !objectNl.isEmpty() ? "object_nl" : + !arrayNl.isEmpty() ? "array_nl" : + asciiOnly ? "ascii_only" : + scriptSafe ? "script_safe" : + allowNaN ? "allow_nan" : null; + if (option != null) { + throw context.runtime.newArgumentError(option + " cannot be used with rfc8785"); + } + } + public int getDepth() { return depth; } @@ -603,6 +619,7 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) { rfc8785 = opts.getBool("rfc8785", rfc8785); opts.ensureEmpty(); + validateRfc8785(context); return this; } diff --git a/lib/json.rb b/lib/json.rb index 72ca9f1d..1969b026 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -418,6 +418,10 @@ # as defined in RFC8785. # Integers are converted to Float before formatting and may be rounded. # Integers outside the finite Float range raise JSON::GeneratorError. +# Nonempty +indent+, +space+, +space_before+, +object_nl+, or +array_nl+, +# and enabled +ascii_only+, +script_safe+, or +allow_nan+ raise ArgumentError. +# JSON.dump defaults +allow_nan+ to +false+ when +rfc8785+ is enabled. +# JSON::Fragment contents are inserted as is; the caller must ensure they are canonical. # # In this example, +obj+ is used first to generate the shortest # \JSON data (no whitespace), then again with all formatting options diff --git a/lib/json/common.rb b/lib/json/common.rb index 11f56408..01a4be18 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -763,7 +763,7 @@ def dump(obj, anIO = nil, _deprecated_limit = nil, kwargs = nil) end opts = { - allow_nan: true, + allow_nan: !(kwargs && kwargs[:rfc8785]), } opts[:max_nesting] = _deprecated_limit if _deprecated_limit opts.merge!(kwargs) if kwargs diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 748a18c1..895b09e7 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -127,7 +127,7 @@ def self.from_state(opts) if opts case when self === opts - return opts + return opts.validate_rfc8785 when opts.respond_to?(:to_hash) return new(opts.to_hash) when opts.respond_to?(:to_h) @@ -308,6 +308,22 @@ def rfc8785? @rfc8785 end + def validate_rfc8785 # :nodoc: + if @rfc8785 + option = if !@indent.empty? then :indent + elsif !@space.empty? then :space + elsif !@space_before.empty? then :space_before + elsif !@object_nl.empty? then :object_nl + elsif !@array_nl.empty? then :array_nl + elsif @ascii_only then :ascii_only + elsif @script_safe then :script_safe + elsif @allow_nan then :allow_nan + end + raise ArgumentError, "#{option} cannot be used with rfc8785" if option + end + self + end + # Configure this State instance with the Hash _opts_, and return # itself. def configure(options) @@ -359,7 +375,7 @@ def configure(options) @max_nesting = max_nesting || 0 self.rfc8785 = rfc8785 - self + validate_rfc8785 end def allow_duplicate_key? # :nodoc: @@ -399,6 +415,7 @@ def generate(obj, anIO = nil) return dup.generate(obj, anIO) if frozen? depth = @depth + validate_rfc8785 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) and !@sort_keys result = generate_json(obj, ''.dup) @@ -542,10 +559,12 @@ module Hash def to_json(state = nil, *) state = State.from_state(state) depth = state.depth - state.check_max_nesting - json_transform(state) - ensure - state.depth = depth + begin + state.check_max_nesting + json_transform(state) + ensure + state.depth = depth + end end private @@ -642,10 +661,12 @@ module Array def to_json(state = nil, *) state = State.from_state(state) depth = state.depth - state.check_max_nesting - json_transform(state) - ensure - state.depth = depth + begin + state.check_max_nesting + json_transform(state) + ensure + state.depth = depth + end end private diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index ee781194..a5f697de 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -1325,6 +1325,62 @@ def test_rfc8785_rejects_out_of_range_integers end end + { + indent: ' ', space: ' ', space_before: ' ', object_nl: "\n", array_nl: "\n", + ascii_only: true, script_safe: true, allow_nan: true, + }.each do |option, value| + define_method("test_rfc8785_rejects_#{option}") do + options = { rfc8785: true, option => value } + error = assert_raise(ArgumentError) { JSON.generate({ 'a' => [1] }, options) } + assert_equal "#{option} cannot be used with rfc8785", error.message + assert_raise(ArgumentError) { JSON.generate(nil, options.to_a.reverse.to_h) } + assert_raise(ArgumentError) { [1].to_json(options) } + assert_raise(ArgumentError) { { 'a' => 1 }.to_json(options) } + assert_raise(ArgumentError) { JSON::Coder.new(**options).dump([1]) } + + state = JSON::State.new(option => value) + state.rfc8785 = true + io = StringIO.new + assert_raise(ArgumentError) { state.generate([1], io) } + assert_equal '', io.string + assert_raise(ArgumentError) { [1].to_json(state) } + assert_equal 0, state.depth + assert_raise(ArgumentError) { state.freeze.generate([1]) } + end + end + + def test_rfc8785_accepts_disabled_options + options = { rfc8785: true, indent: '', space: '', space_before: '', + object_nl: '', array_nl: '', ascii_only: false, + script_safe: false, allow_nan: false } + assert_equal '["é/",1]', JSON.generate(['é/', 1], options) + end + + def test_rfc8785_rejects_pretty_generation + assert_raise(ArgumentError) { JSON.pretty_generate({ 'a' => 1 }, rfc8785: true) } + end + + def test_rfc8785_dump_disables_allow_nan_by_default + assert_equal '[1]', JSON.dump([1], rfc8785: true) + [Float::NAN, Float::INFINITY, -Float::INFINITY].each do |number| + assert_raise(JSON::GeneratorError) { JSON.dump([number], rfc8785: true) } + end + assert_raise(ArgumentError) { JSON.dump([1], rfc8785: true, allow_nan: true) } + end + + def test_rfc8785_fragments + fragment = JSON::Fragment.new('[1,2]') + assert_rfc8785 '[1,2]', fragment + assert_rfc8785 '[[1,2]]', [fragment] + assert_rfc8785 '{"a":[1,2]}', { 'a' => fragment } + assert_equal '[[1,2]]', JSON.generate([fragment], rfc8785: true, max_nesting: false) + assert_equal '[1,2]', JSON::Coder.new(rfc8785: true).dump(fragment) + assert_equal '[1,2]', fragment.to_json(rfc8785: true) + + # Fragment contents are the caller's responsibility and are inserted as is. + assert_rfc8785 '[1, 2]', JSON::Fragment.new('[1, 2]') + end + def test_rfc8785_numbers assert_rfc8785 '-9007199254740992', -9007199254740992 assert_rfc8785 '0', 0