Skip to content

Commit

Permalink
Merge branch 'strict-dump' of github.com:Shopify/json into Shopify-st…
Browse files Browse the repository at this point in the history
…rict-dump
  • Loading branch information
hsbt committed Dec 1, 2023
2 parents 8741567 + f65f228 commit 978ee15
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 8 deletions.
44 changes: 42 additions & 2 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ static ID i_to_s, i_to_json, i_new, i_indent, i_space, i_space_before,
i_object_nl, i_array_nl, i_max_nesting, i_allow_nan, i_ascii_only,
i_pack, i_unpack, i_create_id, i_extend, i_key_p,
i_aref, i_send, i_respond_to_p, i_match, i_keys, i_depth,
i_buffer_initial_length, i_dup, i_script_safe, i_escape_slash;
i_buffer_initial_length, i_dup, i_script_safe, i_escape_slash, i_strict;

/*
* Copyright 2001-2004 Unicode, Inc.
Expand Down Expand Up @@ -749,6 +749,8 @@ static VALUE cState_configure(VALUE self, VALUE opts)
tmp = rb_hash_aref(opts, ID2SYM(i_escape_slash));
state->script_safe = RTEST(tmp);
}
tmp = rb_hash_aref(opts, ID2SYM(i_strict));
state->strict = RTEST(tmp);
return self;
}

Expand Down Expand Up @@ -784,6 +786,7 @@ static VALUE cState_to_h(VALUE self)
rb_hash_aset(result, ID2SYM(i_ascii_only), state->ascii_only ? Qtrue : Qfalse);
rb_hash_aset(result, ID2SYM(i_max_nesting), LONG2FIX(state->max_nesting));
rb_hash_aset(result, ID2SYM(i_script_safe), state->script_safe ? Qtrue : Qfalse);
rb_hash_aset(result, ID2SYM(i_strict), state->strict ? Qtrue : Qfalse);
rb_hash_aset(result, ID2SYM(i_depth), LONG2FIX(state->depth));
rb_hash_aset(result, ID2SYM(i_buffer_initial_length), LONG2FIX(state->buffer_initial_length));
return result;
Expand Down Expand Up @@ -1049,6 +1052,8 @@ static void generate_json(FBuffer *buffer, VALUE Vstate, JSON_Generator_State *s
generate_json_bignum(buffer, Vstate, state, obj);
} else if (klass == rb_cFloat) {
generate_json_float(buffer, Vstate, state, obj);
} else if (state->strict) {
rb_raise(eGeneratorError, "%"PRIsVALUE" not allowed in JSON", RB_OBJ_STRING(CLASS_OF(obj)));
} else if (rb_respond_to(obj, i_to_json)) {
tmp = rb_funcall(obj, i_to_json, 1, Vstate);
Check_Type(tmp, T_STRING);
Expand Down Expand Up @@ -1423,7 +1428,7 @@ static VALUE cState_script_safe(VALUE self)
}

/*
* call-seq: script_safe=(depth)
* call-seq: script_safe=(enable)
*
* This sets whether or not the forward slashes will be escaped in
* the json output.
Expand All @@ -1435,6 +1440,37 @@ static VALUE cState_script_safe_set(VALUE self, VALUE enable)
return Qnil;
}

/*
* call-seq: strict
*
* If this boolean is false, types unsupported by the JSON format will
* be serialized as strings.
* If this boolean is true, types unsupported by the JSON format will
* raise a JSON::GeneratorError.
*/
static VALUE cState_strict(VALUE self)
{
GET_STATE(self);
return state->strict ? Qtrue : Qfalse;
}

/*
* call-seq: strict=(enable)
*
* This sets whether or not to serialize types unsupported by the
* JSON format as strings.
* If this boolean is false, types unsupported by the JSON format will
* be serialized as strings.
* If this boolean is true, types unsupported by the JSON format will
* raise a JSON::GeneratorError.
*/
static VALUE cState_strict_set(VALUE self, VALUE enable)
{
GET_STATE(self);
state->strict = RTEST(enable);
return Qnil;
}

/*
* call-seq: allow_nan?
*
Expand Down Expand Up @@ -1557,6 +1593,9 @@ void Init_generator(void)
rb_define_alias(cState, "escape_slash", "script_safe");
rb_define_alias(cState, "escape_slash?", "script_safe?");
rb_define_alias(cState, "escape_slash=", "script_safe=");
rb_define_method(cState, "strict", cState_strict, 0);
rb_define_method(cState, "strict?", cState_strict, 0);
rb_define_method(cState, "strict=", cState_strict_set, 1);
rb_define_method(cState, "check_circular?", cState_check_circular_p, 0);
rb_define_method(cState, "allow_nan?", cState_allow_nan_p, 0);
rb_define_method(cState, "ascii_only?", cState_ascii_only_p, 0);
Expand Down Expand Up @@ -1615,6 +1654,7 @@ void Init_generator(void)
i_max_nesting = rb_intern("max_nesting");
i_script_safe = rb_intern("script_safe");
i_escape_slash = rb_intern("escape_slash");
i_strict = rb_intern("strict");
i_allow_nan = rb_intern("allow_nan");
i_ascii_only = rb_intern("ascii_only");
i_depth = rb_intern("depth");
Expand Down
3 changes: 3 additions & 0 deletions ext/json/ext/generator/generator.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ typedef struct JSON_Generator_StateStruct {
char allow_nan;
char ascii_only;
char script_safe;
char strict;
long depth;
long buffer_initial_length;
} JSON_Generator_State;
Expand Down Expand Up @@ -153,6 +154,8 @@ static VALUE cState_depth(VALUE self);
static VALUE cState_depth_set(VALUE self, VALUE depth);
static VALUE cState_script_safe(VALUE self);
static VALUE cState_script_safe_set(VALUE self, VALUE depth);
static VALUE cState_strict(VALUE self);
static VALUE cState_strict_set(VALUE self, VALUE strict);
static FBuffer *cState_prepare_buffer(VALUE self);
#ifndef ZALLOC
#define ZALLOC(type) ((type *)ruby_zalloc(sizeof(type)))
Expand Down
6 changes: 5 additions & 1 deletion java/src/json/ext/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,11 @@ void generate(Session session, IRubyObject object, ByteList buffer) {
new Handler<IRubyObject>() {
@Override
RubyString generateNew(Session session, IRubyObject object) {
if (object.respondsTo("to_json")) {
if (session.getState().strict()) {
throw Utils.newException(session.getContext(),
Utils.M_GENERATOR_ERROR,
object + " not allowed in JSON");
} else if (object.respondsTo("to_json")) {
IRubyObject result = object.callMethod(session.getContext(), "to_json",
new IRubyObject[] {session.getState()});
if (result instanceof RubyString) return (RubyString)result;
Expand Down
27 changes: 27 additions & 0 deletions java/src/json/ext/GeneratorState.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ public class GeneratorState extends RubyObject {
*/
private boolean scriptSafe = DEFAULT_SCRIPT_SAFE;
static final boolean DEFAULT_SCRIPT_SAFE = false;
/**
* If set to <code>true</code> types unsupported by the JSON format will
* raise a <code>JSON::GeneratorError</code>.
*/
private boolean strict = DEFAULT_STRICT;
static final boolean DEFAULT_STRICT = false;
/**
* The initial buffer length of this state. (This isn't really used on all
* non-C implementations.)
Expand Down Expand Up @@ -204,6 +210,7 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) {
this.asciiOnly = orig.asciiOnly;
this.quirksMode = orig.quirksMode;
this.scriptSafe = orig.scriptSafe;
this.strict = orig.strict;
this.bufferInitialLength = orig.bufferInitialLength;
this.depth = orig.depth;
return this;
Expand Down Expand Up @@ -379,6 +386,24 @@ public RubyBoolean script_safe_p(ThreadContext context) {
return context.getRuntime().newBoolean(scriptSafe);
}

/**
* Returns true if strict mode is enabled.
*/
public boolean strict() {
return strict;
}

@JRubyMethod(name="strict")
public RubyBoolean strict_get(ThreadContext context) {
return context.getRuntime().newBoolean(strict);
}

@JRubyMethod(name="strict=")
public IRubyObject strict_set(IRubyObject isStrict) {
strict = isStrict.isTrue();
return isStrict.getRuntime().newBoolean(strict);
}

public boolean allowNaN() {
return allowNaN;
}
Expand Down Expand Up @@ -467,6 +492,7 @@ public IRubyObject configure(ThreadContext context, IRubyObject vOpts) {
if (!scriptSafe) {
scriptSafe = opts.getBool("escape_slash", DEFAULT_SCRIPT_SAFE);
}
strict = opts.getBool("strict", DEFAULT_STRICT);
bufferInitialLength = opts.getInt("buffer_initial_length", DEFAULT_BUFFER_INITIAL_LENGTH);

depth = opts.getInt("depth", 0);
Expand Down Expand Up @@ -495,6 +521,7 @@ public RubyHash to_h(ThreadContext context) {
result.op_aset(context, runtime.newSymbol("ascii_only"), ascii_only_p(context));
result.op_aset(context, runtime.newSymbol("max_nesting"), max_nesting_get(context));
result.op_aset(context, runtime.newSymbol("script_safe"), script_safe_get(context));
result.op_aset(context, runtime.newSymbol("strict"), strict_get(context));
result.op_aset(context, runtime.newSymbol("depth"), depth_get(context));
result.op_aset(context, runtime.newSymbol("buffer_initial_length"), buffer_initial_length_get(context));
for (String name: getInstanceVariableNameList()) {
Expand Down
6 changes: 5 additions & 1 deletion lib/json/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
require 'json/generic_object'

module JSON
NOT_SET = Object.new.freeze
private_constant :NOT_SET

class << self
# :call-seq:
# JSON[object] -> new_array or new_string
Expand Down Expand Up @@ -608,7 +611,7 @@ class << self
# puts File.read(path)
# Output:
# {"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"}
def dump(obj, anIO = nil, limit = nil)
def dump(obj, anIO = nil, limit = nil, strict: NOT_SET)
if anIO and limit.nil?
anIO = anIO.to_io if anIO.respond_to?(:to_io)
unless anIO.respond_to?(:write)
Expand All @@ -618,6 +621,7 @@ def dump(obj, anIO = nil, limit = nil)
end
opts = JSON.dump_default_options
opts = opts.merge(:max_nesting => limit) if limit
opts[:strict] = strict if NOT_SET != strict
result = generate(obj, opts)
if anIO
anIO.write result
Expand Down
28 changes: 25 additions & 3 deletions lib/json/pure/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def initialize(opts = {})
@allow_nan = false
@ascii_only = false
@script_safe = false
@strict = false
@buffer_initial_length = 1024
configure opts
end
Expand Down Expand Up @@ -170,6 +171,10 @@ def initialize(opts = {})
# all json strings.
attr_accessor :script_safe

# If this attribute is set to true, attempting to serialize types not
# supported by the JSON spec will raise a JSON::GeneratorError
attr_accessor :strict

# :stopdoc:
attr_reader :buffer_initial_length

Expand Down Expand Up @@ -214,6 +219,11 @@ def script_safe?
@script_safe
end

# Returns true, if forward slashes are escaped. Otherwise returns false.
def strict?
@strict
end

# Configure this State instance with the Hash _opts_, and return
# itself.
def configure(opts)
Expand Down Expand Up @@ -245,6 +255,8 @@ def configure(opts)
false
end

@strict = !!opts[:strict] if opts.key?(:strict)

if !opts.key?(:max_nesting) # defaults to 100
@max_nesting = 100
elsif opts[:max_nesting]
Expand Down Expand Up @@ -304,7 +316,13 @@ module Object
# Converts this object to a string (calling #to_s), converts
# it to a JSON string, and returns the result. This is a fallback, if no
# special method #to_json was defined for some object.
def to_json(*) to_s.to_json end
def to_json(generator_state)
if generator_state.strict?
raise GeneratorError, "#{self.class} not allowed in JSON"
else
to_s.to_json
end
end
end

module Hash
Expand Down Expand Up @@ -336,7 +354,9 @@ def json_transform(state)
result << delim unless first
result << state.indent * depth if indent
result = "#{result}#{key.to_s.to_json(state)}#{state.space_before}:#{state.space}"
if value.respond_to?(:to_json)
if state.strict?
raise GeneratorError, "#{value.class} not allowed in JSON"
elsif value.respond_to?(:to_json)
result << value.to_json(state)
else
result << %{"#{String(value)}"}
Expand Down Expand Up @@ -377,7 +397,9 @@ def json_transform(state)
each { |value|
result << delim unless first
result << state.indent * depth if indent
if value.respond_to?(:to_json)
if state.strict?
raise GeneratorError, "#{value.class} not allowed in JSON"
elsif value.respond_to?(:to_json)
result << value.to_json(state)
else
result << %{"#{String(value)}"}
Expand Down
11 changes: 10 additions & 1 deletion tests/json_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def test_pretty_state
:buffer_initial_length => 1024,
:depth => 0,
:script_safe => false,
:strict => false,
:indent => " ",
:max_nesting => 100,
:object_nl => "\n",
Expand All @@ -167,6 +168,7 @@ def test_safe_state
:buffer_initial_length => 1024,
:depth => 0,
:script_safe => false,
:strict => false,
:indent => "",
:max_nesting => 100,
:object_nl => "",
Expand All @@ -184,6 +186,7 @@ def test_fast_state
:buffer_initial_length => 1024,
:depth => 0,
:script_safe => false,
:strict => false,
:indent => "",
:max_nesting => 0,
:object_nl => "",
Expand Down Expand Up @@ -336,7 +339,13 @@ def test_hash_likeness_set_string

def test_json_generate
assert_raise JSON::GeneratorError do
assert_equal true, generate(["\xea"])
generate(["\xea"])
end
end

def test_json_generate_unsupported_types
assert_raise JSON::GeneratorError do
generate(Object.new, strict: true)
end
end

Expand Down

0 comments on commit 978ee15

Please sign in to comment.