diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index bdc34b71..32f38a58 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/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/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index 0f6bc824..f191993d 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -174,8 +174,8 @@ private static void generateFor(ThreadContext context, S case TRUE : buffer.write(TRUE_STRING); return; case FALSE : buffer.write(FALSE_STRING); return; case FLOAT : generateFloat(context, session, (RubyFloat) object, buffer); return; - case FIXNUM : generateFixnum(session, (RubyFixnum) object, buffer); return; - case BIGNUM : generateBignum((RubyBignum) object, buffer); return; + case FIXNUM : generateFixnum(context, session, (RubyFixnum) object, buffer); return; + case BIGNUM : generateBignum(context, session, (RubyBignum) object, buffer); return; case SYMBOL : generateSymbol(context, session, (RubySymbol) object, buffer); return; case STRING : if (Helpers.metaclass(object) != context.runtime.getString()) break; @@ -345,11 +345,15 @@ void generate(ThreadContext context, Session session, T object, OutputStream buf private static class BignumHandler extends Handler { @Override void generate(ThreadContext context, Session session, RubyBignum object, OutputStream buffer) throws IOException { - generateBignum(object, buffer); + generateBignum(context, session, object, buffer); } } - private static void generateBignum(RubyBignum object, OutputStream buffer) throws IOException { + private static void generateBignum(ThreadContext context, Session session, RubyBignum object, OutputStream buffer) throws IOException { + if (session.getState(context).rfc8785()) { + generateRfc8785Number(context, object, buffer); + return; + } BigInteger bigInt = object.getValue(); buffer.write(bigInt.toString().getBytes(UTF_8)); } @@ -357,11 +361,15 @@ private static void generateBignum(RubyBignum object, OutputStream buffer) throw private static class FixnumHandler extends Handler { @Override void generate(ThreadContext context, Session session, RubyFixnum object, OutputStream buffer) throws IOException { - generateFixnum(session, object, buffer); + generateFixnum(context, session, object, buffer); } } - static void generateFixnum(Session session, RubyFixnum object, OutputStream buffer) throws IOException { + static void generateFixnum(ThreadContext context, Session session, RubyFixnum object, OutputStream buffer) throws IOException { + if (session.getState(context).rfc8785()) { + generateRfc8785Number(context, object, buffer); + return; + } long i = object.getLongValue(); if (i == 0) { buffer.write('0'); @@ -421,14 +429,18 @@ static void generateFloat(ThreadContext context, Session session, RubyFloat obje } if (state.rfc8785()) { - RubyProc numberProc = (RubyProc)GeneratorState.rfc8785NumberFormatterProc; - RubyString numberString = (RubyString)Helpers.invoke(context, numberProc, "call", object); - buffer.write(numberString.toString().getBytes(UTF_8)); + generateRfc8785Number(context, object, buffer); } else { buffer.write(Double.toString(value).getBytes(UTF_8)); } } + private static void generateRfc8785Number(ThreadContext context, IRubyObject object, OutputStream buffer) throws IOException { + RubyProc numberProc = (RubyProc)GeneratorState.rfc8785NumberFormatterProc; + RubyString numberString = (RubyString)Helpers.invoke(context, numberProc, "call", object); + buffer.write(numberString.toString().getBytes(UTF_8)); + } + private static final byte[] EMPTY_ARRAY_BYTES = "[]".getBytes(); private static class ArrayHandler extends Handler> { @Override diff --git a/lib/json.rb b/lib/json.rb index 576d86de..72ca9f1d 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -416,6 +416,8 @@ # 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/lib/json/ext.rb b/lib/json/ext.rb index 4443d668..7382867a 100644 --- a/lib/json/ext.rb +++ b/lib/json/ext.rb @@ -60,6 +60,12 @@ def shareable_lambda(block) # :nodoc: # 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/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index 852ad4e5..748a18c1 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -473,7 +473,7 @@ def generate(obj, anIO = nil) buf << obj.to_json(self) end when Integer - buf << obj.to_s + buf << (@rfc8785 ? State.rfc8785_number_formatter_proc.call(obj) : obj.to_s) when Symbol if @strict fast_serialize_string(obj.name, buf) @@ -698,7 +698,13 @@ def json_transform(state) module Integer # Returns a JSON string representation for this Integer number. - def to_json(*) to_s end + def to_json(state = nil, *) + if state && State.from_state(state).rfc8785? + State.rfc8785_number_formatter_proc.call(self) + else + to_s + end + end end module Float diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 02cff38c..ee781194 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -1283,6 +1283,48 @@ def test_rfc8785_key_order_with_default_external_encoding $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