Skip to content

Commit 978ee15

Browse files
committed
Merge branch 'strict-dump' of github.com:Shopify/json into Shopify-strict-dump
2 parents 8741567 + f65f228 commit 978ee15

File tree

7 files changed

+117
-8
lines changed

7 files changed

+117
-8
lines changed

ext/json/ext/generator/generator.c

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ static ID i_to_s, i_to_json, i_new, i_indent, i_space, i_space_before,
1616
i_object_nl, i_array_nl, i_max_nesting, i_allow_nan, i_ascii_only,
1717
i_pack, i_unpack, i_create_id, i_extend, i_key_p,
1818
i_aref, i_send, i_respond_to_p, i_match, i_keys, i_depth,
19-
i_buffer_initial_length, i_dup, i_script_safe, i_escape_slash;
19+
i_buffer_initial_length, i_dup, i_script_safe, i_escape_slash, i_strict;
2020

2121
/*
2222
* Copyright 2001-2004 Unicode, Inc.
@@ -749,6 +749,8 @@ static VALUE cState_configure(VALUE self, VALUE opts)
749749
tmp = rb_hash_aref(opts, ID2SYM(i_escape_slash));
750750
state->script_safe = RTEST(tmp);
751751
}
752+
tmp = rb_hash_aref(opts, ID2SYM(i_strict));
753+
state->strict = RTEST(tmp);
752754
return self;
753755
}
754756

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

14251430
/*
1426-
* call-seq: script_safe=(depth)
1431+
* call-seq: script_safe=(enable)
14271432
*
14281433
* This sets whether or not the forward slashes will be escaped in
14291434
* the json output.
@@ -1435,6 +1440,37 @@ static VALUE cState_script_safe_set(VALUE self, VALUE enable)
14351440
return Qnil;
14361441
}
14371442

1443+
/*
1444+
* call-seq: strict
1445+
*
1446+
* If this boolean is false, types unsupported by the JSON format will
1447+
* be serialized as strings.
1448+
* If this boolean is true, types unsupported by the JSON format will
1449+
* raise a JSON::GeneratorError.
1450+
*/
1451+
static VALUE cState_strict(VALUE self)
1452+
{
1453+
GET_STATE(self);
1454+
return state->strict ? Qtrue : Qfalse;
1455+
}
1456+
1457+
/*
1458+
* call-seq: strict=(enable)
1459+
*
1460+
* This sets whether or not to serialize types unsupported by the
1461+
* JSON format as strings.
1462+
* If this boolean is false, types unsupported by the JSON format will
1463+
* be serialized as strings.
1464+
* If this boolean is true, types unsupported by the JSON format will
1465+
* raise a JSON::GeneratorError.
1466+
*/
1467+
static VALUE cState_strict_set(VALUE self, VALUE enable)
1468+
{
1469+
GET_STATE(self);
1470+
state->strict = RTEST(enable);
1471+
return Qnil;
1472+
}
1473+
14381474
/*
14391475
* call-seq: allow_nan?
14401476
*
@@ -1557,6 +1593,9 @@ void Init_generator(void)
15571593
rb_define_alias(cState, "escape_slash", "script_safe");
15581594
rb_define_alias(cState, "escape_slash?", "script_safe?");
15591595
rb_define_alias(cState, "escape_slash=", "script_safe=");
1596+
rb_define_method(cState, "strict", cState_strict, 0);
1597+
rb_define_method(cState, "strict?", cState_strict, 0);
1598+
rb_define_method(cState, "strict=", cState_strict_set, 1);
15601599
rb_define_method(cState, "check_circular?", cState_check_circular_p, 0);
15611600
rb_define_method(cState, "allow_nan?", cState_allow_nan_p, 0);
15621601
rb_define_method(cState, "ascii_only?", cState_ascii_only_p, 0);
@@ -1615,6 +1654,7 @@ void Init_generator(void)
16151654
i_max_nesting = rb_intern("max_nesting");
16161655
i_script_safe = rb_intern("script_safe");
16171656
i_escape_slash = rb_intern("escape_slash");
1657+
i_strict = rb_intern("strict");
16181658
i_allow_nan = rb_intern("allow_nan");
16191659
i_ascii_only = rb_intern("ascii_only");
16201660
i_depth = rb_intern("depth");

ext/json/ext/generator/generator.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ typedef struct JSON_Generator_StateStruct {
7373
char allow_nan;
7474
char ascii_only;
7575
char script_safe;
76+
char strict;
7677
long depth;
7778
long buffer_initial_length;
7879
} JSON_Generator_State;
@@ -153,6 +154,8 @@ static VALUE cState_depth(VALUE self);
153154
static VALUE cState_depth_set(VALUE self, VALUE depth);
154155
static VALUE cState_script_safe(VALUE self);
155156
static VALUE cState_script_safe_set(VALUE self, VALUE depth);
157+
static VALUE cState_strict(VALUE self);
158+
static VALUE cState_strict_set(VALUE self, VALUE strict);
156159
static FBuffer *cState_prepare_buffer(VALUE self);
157160
#ifndef ZALLOC
158161
#define ZALLOC(type) ((type *)ruby_zalloc(sizeof(type)))

java/src/json/ext/Generator.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,11 @@ void generate(Session session, IRubyObject object, ByteList buffer) {
428428
new Handler<IRubyObject>() {
429429
@Override
430430
RubyString generateNew(Session session, IRubyObject object) {
431-
if (object.respondsTo("to_json")) {
431+
if (session.getState().strict()) {
432+
throw Utils.newException(session.getContext(),
433+
Utils.M_GENERATOR_ERROR,
434+
object + " not allowed in JSON");
435+
} else if (object.respondsTo("to_json")) {
432436
IRubyObject result = object.callMethod(session.getContext(), "to_json",
433437
new IRubyObject[] {session.getState()});
434438
if (result instanceof RubyString) return (RubyString)result;

java/src/json/ext/GeneratorState.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ public class GeneratorState extends RubyObject {
8888
*/
8989
private boolean scriptSafe = DEFAULT_SCRIPT_SAFE;
9090
static final boolean DEFAULT_SCRIPT_SAFE = false;
91+
/**
92+
* If set to <code>true</code> types unsupported by the JSON format will
93+
* raise a <code>JSON::GeneratorError</code>.
94+
*/
95+
private boolean strict = DEFAULT_STRICT;
96+
static final boolean DEFAULT_STRICT = false;
9197
/**
9298
* The initial buffer length of this state. (This isn't really used on all
9399
* non-C implementations.)
@@ -204,6 +210,7 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) {
204210
this.asciiOnly = orig.asciiOnly;
205211
this.quirksMode = orig.quirksMode;
206212
this.scriptSafe = orig.scriptSafe;
213+
this.strict = orig.strict;
207214
this.bufferInitialLength = orig.bufferInitialLength;
208215
this.depth = orig.depth;
209216
return this;
@@ -379,6 +386,24 @@ public RubyBoolean script_safe_p(ThreadContext context) {
379386
return context.getRuntime().newBoolean(scriptSafe);
380387
}
381388

389+
/**
390+
* Returns true if strict mode is enabled.
391+
*/
392+
public boolean strict() {
393+
return strict;
394+
}
395+
396+
@JRubyMethod(name="strict")
397+
public RubyBoolean strict_get(ThreadContext context) {
398+
return context.getRuntime().newBoolean(strict);
399+
}
400+
401+
@JRubyMethod(name="strict=")
402+
public IRubyObject strict_set(IRubyObject isStrict) {
403+
strict = isStrict.isTrue();
404+
return isStrict.getRuntime().newBoolean(strict);
405+
}
406+
382407
public boolean allowNaN() {
383408
return allowNaN;
384409
}
@@ -467,6 +492,7 @@ public IRubyObject configure(ThreadContext context, IRubyObject vOpts) {
467492
if (!scriptSafe) {
468493
scriptSafe = opts.getBool("escape_slash", DEFAULT_SCRIPT_SAFE);
469494
}
495+
strict = opts.getBool("strict", DEFAULT_STRICT);
470496
bufferInitialLength = opts.getInt("buffer_initial_length", DEFAULT_BUFFER_INITIAL_LENGTH);
471497

472498
depth = opts.getInt("depth", 0);
@@ -495,6 +521,7 @@ public RubyHash to_h(ThreadContext context) {
495521
result.op_aset(context, runtime.newSymbol("ascii_only"), ascii_only_p(context));
496522
result.op_aset(context, runtime.newSymbol("max_nesting"), max_nesting_get(context));
497523
result.op_aset(context, runtime.newSymbol("script_safe"), script_safe_get(context));
524+
result.op_aset(context, runtime.newSymbol("strict"), strict_get(context));
498525
result.op_aset(context, runtime.newSymbol("depth"), depth_get(context));
499526
result.op_aset(context, runtime.newSymbol("buffer_initial_length"), buffer_initial_length_get(context));
500527
for (String name: getInstanceVariableNameList()) {

lib/json/common.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
require 'json/generic_object'
44

55
module JSON
6+
NOT_SET = Object.new.freeze
7+
private_constant :NOT_SET
8+
69
class << self
710
# :call-seq:
811
# JSON[object] -> new_array or new_string
@@ -608,7 +611,7 @@ class << self
608611
# puts File.read(path)
609612
# Output:
610613
# {"foo":[0,1],"bar":{"baz":2,"bat":3},"bam":"bad"}
611-
def dump(obj, anIO = nil, limit = nil)
614+
def dump(obj, anIO = nil, limit = nil, strict: NOT_SET)
612615
if anIO and limit.nil?
613616
anIO = anIO.to_io if anIO.respond_to?(:to_io)
614617
unless anIO.respond_to?(:write)
@@ -618,6 +621,7 @@ def dump(obj, anIO = nil, limit = nil)
618621
end
619622
opts = JSON.dump_default_options
620623
opts = opts.merge(:max_nesting => limit) if limit
624+
opts[:strict] = strict if NOT_SET != strict
621625
result = generate(obj, opts)
622626
if anIO
623627
anIO.write result

lib/json/pure/generator.rb

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def initialize(opts = {})
141141
@allow_nan = false
142142
@ascii_only = false
143143
@script_safe = false
144+
@strict = false
144145
@buffer_initial_length = 1024
145146
configure opts
146147
end
@@ -170,6 +171,10 @@ def initialize(opts = {})
170171
# all json strings.
171172
attr_accessor :script_safe
172173

174+
# If this attribute is set to true, attempting to serialize types not
175+
# supported by the JSON spec will raise a JSON::GeneratorError
176+
attr_accessor :strict
177+
173178
# :stopdoc:
174179
attr_reader :buffer_initial_length
175180

@@ -214,6 +219,11 @@ def script_safe?
214219
@script_safe
215220
end
216221

222+
# Returns true, if forward slashes are escaped. Otherwise returns false.
223+
def strict?
224+
@strict
225+
end
226+
217227
# Configure this State instance with the Hash _opts_, and return
218228
# itself.
219229
def configure(opts)
@@ -245,6 +255,8 @@ def configure(opts)
245255
false
246256
end
247257

258+
@strict = !!opts[:strict] if opts.key?(:strict)
259+
248260
if !opts.key?(:max_nesting) # defaults to 100
249261
@max_nesting = 100
250262
elsif opts[:max_nesting]
@@ -304,7 +316,13 @@ module Object
304316
# Converts this object to a string (calling #to_s), converts
305317
# it to a JSON string, and returns the result. This is a fallback, if no
306318
# special method #to_json was defined for some object.
307-
def to_json(*) to_s.to_json end
319+
def to_json(generator_state)
320+
if generator_state.strict?
321+
raise GeneratorError, "#{self.class} not allowed in JSON"
322+
else
323+
to_s.to_json
324+
end
325+
end
308326
end
309327

310328
module Hash
@@ -336,7 +354,9 @@ def json_transform(state)
336354
result << delim unless first
337355
result << state.indent * depth if indent
338356
result = "#{result}#{key.to_s.to_json(state)}#{state.space_before}:#{state.space}"
339-
if value.respond_to?(:to_json)
357+
if state.strict?
358+
raise GeneratorError, "#{value.class} not allowed in JSON"
359+
elsif value.respond_to?(:to_json)
340360
result << value.to_json(state)
341361
else
342362
result << %{"#{String(value)}"}
@@ -377,7 +397,9 @@ def json_transform(state)
377397
each { |value|
378398
result << delim unless first
379399
result << state.indent * depth if indent
380-
if value.respond_to?(:to_json)
400+
if state.strict?
401+
raise GeneratorError, "#{value.class} not allowed in JSON"
402+
elsif value.respond_to?(:to_json)
381403
result << value.to_json(state)
382404
else
383405
result << %{"#{String(value)}"}

tests/json_generator_test.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def test_pretty_state
150150
:buffer_initial_length => 1024,
151151
:depth => 0,
152152
:script_safe => false,
153+
:strict => false,
153154
:indent => " ",
154155
:max_nesting => 100,
155156
:object_nl => "\n",
@@ -167,6 +168,7 @@ def test_safe_state
167168
:buffer_initial_length => 1024,
168169
:depth => 0,
169170
:script_safe => false,
171+
:strict => false,
170172
:indent => "",
171173
:max_nesting => 100,
172174
:object_nl => "",
@@ -184,6 +186,7 @@ def test_fast_state
184186
:buffer_initial_length => 1024,
185187
:depth => 0,
186188
:script_safe => false,
189+
:strict => false,
187190
:indent => "",
188191
:max_nesting => 0,
189192
:object_nl => "",
@@ -336,7 +339,13 @@ def test_hash_likeness_set_string
336339

337340
def test_json_generate
338341
assert_raise JSON::GeneratorError do
339-
assert_equal true, generate(["\xea"])
342+
generate(["\xea"])
343+
end
344+
end
345+
346+
def test_json_generate_unsupported_types
347+
assert_raise JSON::GeneratorError do
348+
generate(Object.new, strict: true)
340349
end
341350
end
342351

0 commit comments

Comments
 (0)