Skip to content

Commit 5ebb52f

Browse files
committed
Reject incompatible RFC 8785 generation options
1 parent 193df55 commit 5ebb52f

7 files changed

Lines changed: 130 additions & 11 deletions

File tree

‎ext/json/ext/generator/generator.c‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,10 +1294,28 @@ static void generate_json_no_fallback(FBuffer *buffer, struct generate_json_data
12941294
generate_json_general(buffer, data, obj, false);
12951295
}
12961296

1297+
static void validate_rfc8785(JSON_Generator_State *state)
1298+
{
1299+
if (!state->rfc8785) return;
1300+
1301+
const char *option = state->indent ? "indent" :
1302+
state->space ? "space" :
1303+
state->space_before ? "space_before" :
1304+
state->object_nl ? "object_nl" :
1305+
state->array_nl ? "array_nl" :
1306+
state->ascii_only ? "ascii_only" :
1307+
state->script_safe ? "script_safe" :
1308+
state->allow_nan ? "allow_nan" : NULL;
1309+
if (option) {
1310+
rb_raise(rb_eArgError, "%s cannot be used with rfc8785", option);
1311+
}
1312+
}
1313+
12971314
static VALUE generate_json_try(VALUE d)
12981315
{
12991316
struct generate_json_data *data = (struct generate_json_data *)d;
13001317

1318+
validate_rfc8785(data->state);
13011319
data->func(data->buffer, data, data->obj);
13021320

13031321
return fbuffer_finalize(data->buffer);
@@ -1977,6 +1995,7 @@ static void configure_state(JSON_Generator_State *state, VALUE vstate, VALUE con
19771995
rb_hash_foreach(config, configure_state_i, (VALUE)&data);
19781996

19791997
raise_argument_error_on_unknown_keywords(data.unknown_keywords);
1998+
validate_rfc8785(state);
19801999
}
19812000

19822001
static VALUE cState_configure(VALUE self, VALUE opts)

‎java/src/json/ext/Generator.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T
5656
static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T object, Handler<? super T> handler, IRubyObject arg0) {
5757
Session session = new Session(arg0);
5858
GeneratorState state = session.getState(context);
59+
state.validateRfc8785(context);
5960
int depth = state.depth;
6061
try {
6162
return handler.generateNew(context, session, object);
@@ -87,6 +88,7 @@ static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T
8788
GeneratorState config, IRubyObject io) {
8889
Session session = new Session(config);
8990
GeneratorState state = session.getState(context);
91+
state.validateRfc8785(context);
9092
int depth = state.depth;
9193

9294
try {

‎java/src/json/ext/GeneratorState.java‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,22 @@ public boolean rfc8785() {
525525
return this.rfc8785;
526526
}
527527

528+
public void validateRfc8785(ThreadContext context) {
529+
if (!rfc8785) return;
530+
531+
String option = !indent.isEmpty() ? "indent" :
532+
!space.isEmpty() ? "space" :
533+
!spaceBefore.isEmpty() ? "space_before" :
534+
!objectNl.isEmpty() ? "object_nl" :
535+
!arrayNl.isEmpty() ? "array_nl" :
536+
asciiOnly ? "ascii_only" :
537+
scriptSafe ? "script_safe" :
538+
allowNaN ? "allow_nan" : null;
539+
if (option != null) {
540+
throw context.runtime.newArgumentError(option + " cannot be used with rfc8785");
541+
}
542+
}
543+
528544
public int getDepth() {
529545
return depth;
530546
}
@@ -603,6 +619,7 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) {
603619
rfc8785 = opts.getBool("rfc8785", rfc8785);
604620

605621
opts.ensureEmpty();
622+
validateRfc8785(context);
606623

607624
return this;
608625
}

‎lib/json.rb‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,10 @@
416416
# order, allowing for arbitrary sort orders.
417417
# - Option +rfc8785+ (boolean) controls whether the generated JSON will be canonicalized
418418
# as defined in RFC8785.
419+
# Nonempty +indent+, +space+, +space_before+, +object_nl+, or +array_nl+,
420+
# and enabled +ascii_only+, +script_safe+, or +allow_nan+ raise ArgumentError.
421+
# JSON.dump defaults +allow_nan+ to +false+ when +rfc8785+ is enabled.
422+
# JSON::Fragment contents are inserted as is; the caller must ensure they are canonical.
419423
#
420424
# In this example, +obj+ is used first to generate the shortest
421425
# \JSON data (no whitespace), then again with all formatting options

‎lib/json/common.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -763,7 +763,7 @@ def dump(obj, anIO = nil, _deprecated_limit = nil, kwargs = nil)
763763
end
764764

765765
opts = {
766-
allow_nan: true,
766+
allow_nan: !(kwargs && kwargs[:rfc8785]),
767767
}
768768
opts[:max_nesting] = _deprecated_limit if _deprecated_limit
769769
opts.merge!(kwargs) if kwargs

‎lib/json/truffle_ruby/generator.rb‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def self.from_state(opts)
127127
if opts
128128
case
129129
when self === opts
130-
return opts
130+
return opts.validate_rfc8785
131131
when opts.respond_to?(:to_hash)
132132
return new(opts.to_hash)
133133
when opts.respond_to?(:to_h)
@@ -308,6 +308,22 @@ def rfc8785?
308308
@rfc8785
309309
end
310310

311+
def validate_rfc8785 # :nodoc:
312+
if @rfc8785
313+
option = if !@indent.empty? then :indent
314+
elsif !@space.empty? then :space
315+
elsif !@space_before.empty? then :space_before
316+
elsif !@object_nl.empty? then :object_nl
317+
elsif !@array_nl.empty? then :array_nl
318+
elsif @ascii_only then :ascii_only
319+
elsif @script_safe then :script_safe
320+
elsif @allow_nan then :allow_nan
321+
end
322+
raise ArgumentError, "#{option} cannot be used with rfc8785" if option
323+
end
324+
self
325+
end
326+
311327
# Configure this State instance with the Hash _opts_, and return
312328
# itself.
313329
def configure(options)
@@ -359,7 +375,7 @@ def configure(options)
359375
@max_nesting = max_nesting || 0
360376
self.rfc8785 = rfc8785
361377

362-
self
378+
validate_rfc8785
363379
end
364380

365381
def allow_duplicate_key? # :nodoc:
@@ -399,6 +415,7 @@ def generate(obj, anIO = nil)
399415
return dup.generate(obj, anIO) if frozen?
400416

401417
depth = @depth
418+
validate_rfc8785
402419
if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and
403420
!@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj) and !@sort_keys
404421
result = generate_json(obj, ''.dup)
@@ -542,10 +559,12 @@ module Hash
542559
def to_json(state = nil, *)
543560
state = State.from_state(state)
544561
depth = state.depth
545-
state.check_max_nesting
546-
json_transform(state)
547-
ensure
548-
state.depth = depth
562+
begin
563+
state.check_max_nesting
564+
json_transform(state)
565+
ensure
566+
state.depth = depth
567+
end
549568
end
550569

551570
private
@@ -642,10 +661,12 @@ module Array
642661
def to_json(state = nil, *)
643662
state = State.from_state(state)
644663
depth = state.depth
645-
state.check_max_nesting
646-
json_transform(state)
647-
ensure
648-
state.depth = depth
664+
begin
665+
state.check_max_nesting
666+
json_transform(state)
667+
ensure
668+
state.depth = depth
669+
end
649670
end
650671

651672
private

‎test/json/json_generator_test.rb‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,62 @@ def test_large_depth_raises
12531253
end
12541254
end
12551255

1256+
{
1257+
indent: ' ', space: ' ', space_before: ' ', object_nl: "\n", array_nl: "\n",
1258+
ascii_only: true, script_safe: true, allow_nan: true,
1259+
}.each do |option, value|
1260+
define_method("test_rfc8785_rejects_#{option}") do
1261+
options = { rfc8785: true, option => value }
1262+
error = assert_raise(ArgumentError) { JSON.generate({ 'a' => [1] }, options) }
1263+
assert_equal "#{option} cannot be used with rfc8785", error.message
1264+
assert_raise(ArgumentError) { JSON.generate(nil, options.to_a.reverse.to_h) }
1265+
assert_raise(ArgumentError) { [1].to_json(options) }
1266+
assert_raise(ArgumentError) { { 'a' => 1 }.to_json(options) }
1267+
assert_raise(ArgumentError) { JSON::Coder.new(**options).dump([1]) }
1268+
1269+
state = JSON::State.new(option => value)
1270+
state.rfc8785 = true
1271+
io = StringIO.new
1272+
assert_raise(ArgumentError) { state.generate([1], io) }
1273+
assert_equal '', io.string
1274+
assert_raise(ArgumentError) { [1].to_json(state) }
1275+
assert_equal 0, state.depth
1276+
assert_raise(ArgumentError) { state.freeze.generate([1]) }
1277+
end
1278+
end
1279+
1280+
def test_rfc8785_accepts_disabled_options
1281+
options = { rfc8785: true, indent: '', space: '', space_before: '',
1282+
object_nl: '', array_nl: '', ascii_only: false,
1283+
script_safe: false, allow_nan: false }
1284+
assert_equal '["é/",1]', JSON.generate(['é/', 1], options)
1285+
end
1286+
1287+
def test_rfc8785_rejects_pretty_generation
1288+
assert_raise(ArgumentError) { JSON.pretty_generate({ 'a' => 1 }, rfc8785: true) }
1289+
end
1290+
1291+
def test_rfc8785_dump_disables_allow_nan_by_default
1292+
assert_equal '[1]', JSON.dump([1], rfc8785: true)
1293+
[Float::NAN, Float::INFINITY, -Float::INFINITY].each do |number|
1294+
assert_raise(JSON::GeneratorError) { JSON.dump([number], rfc8785: true) }
1295+
end
1296+
assert_raise(ArgumentError) { JSON.dump([1], rfc8785: true, allow_nan: true) }
1297+
end
1298+
1299+
def test_rfc8785_fragments
1300+
fragment = JSON::Fragment.new('[1,2]')
1301+
assert_rfc8785 '[1,2]', fragment
1302+
assert_rfc8785 '[[1,2]]', [fragment]
1303+
assert_rfc8785 '{"a":[1,2]}', { 'a' => fragment }
1304+
assert_equal '[[1,2]]', JSON.generate([fragment], rfc8785: true, max_nesting: false)
1305+
assert_equal '[1,2]', JSON::Coder.new(rfc8785: true).dump(fragment)
1306+
assert_equal '[1,2]', fragment.to_json(rfc8785: true)
1307+
1308+
# Fragment contents are the caller's responsibility and are inserted as is.
1309+
assert_rfc8785 '[1, 2]', JSON::Fragment.new('[1, 2]')
1310+
end
1311+
12561312
def test_rfc8785_numbers
12571313
assert_rfc8785 '-9007199254740992', -9007199254740992
12581314
assert_rfc8785 '0', 0

0 commit comments

Comments
 (0)