diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index bdc34b71fe3..32f38a58d4e 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -1135,13 +1135,28 @@ static void generate_json_true(FBuffer *buffer, struct generate_json_data *data, fbuffer_append(buffer, "true", 4); } +static void generate_json_rfc8785_number(FBuffer *buffer, VALUE obj) +{ + VALUE str = rb_proc_call_with_block(rfc8785_number_formatter_proc, 1, &obj, Qnil); + Check_Type(str, T_STRING); + fbuffer_append_str(buffer, str); +} + static void generate_json_fixnum(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { + if (RB_UNLIKELY(data->state->rfc8785)) { + generate_json_rfc8785_number(buffer, obj); + return; + } fbuffer_append_long(buffer, FIX2LONG(obj)); } static void generate_json_bignum(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { + if (RB_UNLIKELY(data->state->rfc8785)) { + generate_json_rfc8785_number(buffer, obj); + return; + } VALUE tmp = rb_funcall(obj, i_to_s, 0); fbuffer_append_str(buffer, StringValue(tmp)); } @@ -1171,9 +1186,7 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data } if (RB_UNLIKELY(data->state->rfc8785)) { - VALUE str = rb_proc_call_with_block(rfc8785_number_formatter_proc, 1, &obj, Qnil); - Check_Type(str, T_STRING); - fbuffer_append_str(buffer, str); + generate_json_rfc8785_number(buffer, obj); return; } diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb index 576d86de9ab..72ca9f1de7d 100644 --- a/ext/json/lib/json.rb +++ b/ext/json/lib/json.rb @@ -416,6 +416,8 @@ require 'json/common' # order, allowing for arbitrary sort orders. # - Option +rfc8785+ (boolean) controls whether the generated JSON will be canonicalized # as defined in RFC8785. +# Integers are converted to Float before formatting and may be rounded. +# Integers outside the finite Float range raise JSON::GeneratorError. # # In this example, +obj+ is used first to generate the shortest # \JSON data (no whitespace), then again with all formatting options diff --git a/ext/json/lib/json/ext.rb b/ext/json/lib/json/ext.rb index 4443d668704..7382867a2d6 100644 --- a/ext/json/lib/json/ext.rb +++ b/ext/json/lib/json/ext.rb @@ -60,6 +60,12 @@ module JSON # Directly lifted from Gregg Kellogg's json-canonicalization generator::State.rfc8785_number_formatter_proc = shareable_lambda(->(num) { + if Integer === num + if num > Float::MAX || num < -Float::MAX + raise GeneratorError.new("Integer out of range for RFC 8785", num) + end + num = num.to_f + end if num.zero? "0" else diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 02cff38cda7..ee781194d70 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -1283,6 +1283,48 @@ class JSONGeneratorTest < Test::Unit::TestCase $VERBOSE = verbose end + { + 0 => '0', + 1 => '1', + 2**53 - 1 => '9007199254740991', + 2**53 => '9007199254740992', + 2**53 + 1 => '9007199254740992', + 2**53 + 3 => '9007199254740996', + 2**63 - 1 => '9223372036854776000', + 2**68 => '295147905179352830000', + 10**20 => '100000000000000000000', + 10**21 => '1e+21', + Float::MAX.to_i => '1.7976931348623157e+308', + }.each do |integer, expected| + define_method("test_rfc8785_integer_#{integer}") do + [1, -1].each do |sign| + number = sign * integer + json = sign < 0 && integer != 0 ? "-#{expected}" : expected + assert_rfc8785 json, number + assert_rfc8785 %({"n":[#{json}]}), { 'n' => [number] } + assert_equal json, number.to_json(rfc8785: true) + assert_equal json, JSON.generate(number, rfc8785: true, max_nesting: false) + assert_equal json, JSON::Coder.new(rfc8785: true).dump(number) + io = StringIO.new + JSON.dump(number, io, rfc8785: true) + assert_equal json, io.string + assert_equal number.to_s, JSON.generate(number) + end + end + end + + def test_rfc8785_rejects_out_of_range_integers + [Float::MAX.to_i + 1, 10**400].each do |integer| + [integer, -integer].each do |number| + error = assert_raise(JSON::GeneratorError) { JSON.generate(number, rfc8785: true) } + assert_same number, error.invalid_object + assert_equal 'Integer out of range for RFC 8785', error.message + assert_raise(JSON::GeneratorError) { JSON.dump([number], rfc8785: true) } + assert_equal number.to_s, JSON.generate(number) + end + end + end + def test_rfc8785_numbers assert_rfc8785 '-9007199254740992', -9007199254740992 assert_rfc8785 '0', 0