Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions java/src/json/ext/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T
static <T extends IRubyObject> RubyString generateJson(ThreadContext context, T object, Handler<? super T> 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);
Expand Down Expand Up @@ -87,6 +88,7 @@ static <T extends IRubyObject> 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 {
Expand Down
17 changes: 17 additions & 0 deletions java/src/json/ext/GeneratorState.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -603,6 +619,7 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) {
rfc8785 = opts.getBool("rfc8785", rfc8785);

opts.ensureEmpty();
validateRfc8785(context);

return this;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/json/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 31 additions & 10 deletions lib/json/truffle_ruby/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -359,7 +375,7 @@ def configure(options)
@max_nesting = max_nesting || 0
self.rfc8785 = rfc8785

self
validate_rfc8785
end

def allow_duplicate_key? # :nodoc:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions test/json/json_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading