Skip to content

Commit dbd5042

Browse files
authored
Merge pull request #712 from byroot/generation-error
JSON::GeneratorError expose invalid object
2 parents 55015fa + 03d7414 commit dbd5042

File tree

7 files changed

+119
-50
lines changed

7 files changed

+119
-50
lines changed

ext/json/ext/generator/generator.c

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data
7171

7272
static int usascii_encindex, utf8_encindex, binary_encindex;
7373

74+
#ifdef RBIMPL_ATTR_NORETURN
75+
RBIMPL_ATTR_NORETURN()
76+
#endif
77+
static void raise_generator_error_str(VALUE invalid_object, VALUE str)
78+
{
79+
VALUE exc = rb_exc_new_str(eGeneratorError, str);
80+
rb_ivar_set(exc, rb_intern("@invalid_object"), invalid_object);
81+
rb_exc_raise(exc);
82+
}
83+
84+
#ifdef RBIMPL_ATTR_NORETURN
85+
RBIMPL_ATTR_NORETURN()
86+
#endif
87+
static void raise_generator_error(VALUE invalid_object, const char *fmt, ...)
88+
{
89+
va_list args;
90+
va_start(args, fmt);
91+
VALUE str = rb_vsprintf(fmt, args);
92+
va_end(args);
93+
raise_generator_error_str(invalid_object, str);
94+
}
95+
7496
/* Converts in_string to a JSON string (without the wrapping '"'
7597
* characters) in FBuffer out_buffer.
7698
*
@@ -867,6 +889,17 @@ static inline int enc_utf8_compatible_p(int enc_idx)
867889
return 0;
868890
}
869891

892+
static VALUE encode_json_string_try(VALUE str)
893+
{
894+
return rb_funcall(str, i_encode, 1, Encoding_UTF_8);
895+
}
896+
897+
static VALUE encode_json_string_rescue(VALUE str, VALUE exception)
898+
{
899+
raise_generator_error_str(str, rb_funcall(exception, rb_intern("message"), 0));
900+
return Qundef;
901+
}
902+
870903
static inline VALUE ensure_valid_encoding(VALUE str)
871904
{
872905
int encindex = RB_ENCODING_GET(str);
@@ -886,7 +919,7 @@ static inline VALUE ensure_valid_encoding(VALUE str)
886919
}
887920
}
888921

889-
str = rb_funcall(str, i_encode, 1, Encoding_UTF_8);
922+
str = rb_rescue(encode_json_string_try, str, encode_json_string_rescue, str);
890923
}
891924
return str;
892925
}
@@ -909,7 +942,7 @@ static void generate_json_string(FBuffer *buffer, struct generate_json_data *dat
909942
}
910943
break;
911944
default:
912-
rb_raise(rb_path2class("JSON::GeneratorError"), "source sequence is illegal/malformed utf-8");
945+
raise_generator_error(obj, "source sequence is illegal/malformed utf-8");
913946
break;
914947
}
915948
fbuffer_append_char(buffer, '"');
@@ -957,10 +990,8 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data
957990
char allow_nan = state->allow_nan;
958991
VALUE tmp = rb_funcall(obj, i_to_s, 0);
959992
if (!allow_nan) {
960-
if (isinf(value)) {
961-
rb_raise(eGeneratorError, "%"PRIsVALUE" not allowed in JSON", tmp);
962-
} else if (isnan(value)) {
963-
rb_raise(eGeneratorError, "%"PRIsVALUE" not allowed in JSON", tmp);
993+
if (isinf(value) || isnan(value)) {
994+
raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", tmp);
964995
}
965996
}
966997
fbuffer_append_str(buffer, tmp);
@@ -1008,7 +1039,7 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON
10081039
default:
10091040
general:
10101041
if (state->strict) {
1011-
rb_raise(eGeneratorError, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
1042+
raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
10121043
} else if (rb_respond_to(obj, i_to_json)) {
10131044
tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data));
10141045
Check_Type(tmp, T_STRING);
@@ -1036,10 +1067,6 @@ static VALUE generate_json_rescue(VALUE d, VALUE exc)
10361067
struct generate_json_data *data = (struct generate_json_data *)d;
10371068
fbuffer_free(data->buffer);
10381069

1039-
if (RBASIC_CLASS(exc) == rb_path2class("Encoding::UndefinedConversionError")) {
1040-
exc = rb_exc_new_str(eGeneratorError, rb_funcall(exc, rb_intern("message"), 0));
1041-
}
1042-
10431070
rb_exc_raise(exc);
10441071

10451072
return Qundef;
@@ -1537,10 +1564,11 @@ void Init_generator(void)
15371564
VALUE mExt = rb_define_module_under(mJSON, "Ext");
15381565
VALUE mGenerator = rb_define_module_under(mExt, "Generator");
15391566

1567+
rb_global_variable(&eGeneratorError);
15401568
eGeneratorError = rb_path2class("JSON::GeneratorError");
1569+
1570+
rb_global_variable(&eNestingError);
15411571
eNestingError = rb_path2class("JSON::NestingError");
1542-
rb_gc_register_mark_object(eGeneratorError);
1543-
rb_gc_register_mark_object(eNestingError);
15441572

15451573
cState = rb_define_class_under(mGenerator, "State", rb_cObject);
15461574
rb_define_alloc_func(cState, cState_s_allocate);

java/src/json/ext/Generator.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.jruby.RubyHash;
2121
import org.jruby.RubyString;
2222
import org.jruby.RubySymbol;
23+
import org.jruby.RubyException;
2324
import org.jruby.runtime.Helpers;
2425
import org.jruby.runtime.ThreadContext;
2526
import org.jruby.runtime.builtin.IRubyObject;
@@ -254,7 +255,7 @@ void generate(ThreadContext context, Session session, RubyFloat object, OutputSt
254255

255256
if (Double.isInfinite(value) || Double.isNaN(value)) {
256257
if (!session.getState(context).allowNaN()) {
257-
throw Utils.newException(context, Utils.M_GENERATOR_ERROR, object + " not allowed in JSON");
258+
throw Utils.buildGeneratorError(context, object, object + " not allowed in JSON").toThrowable();
258259
}
259260
}
260261

@@ -429,20 +430,23 @@ int guessSize(ThreadContext context, Session session, RubyString object) {
429430
void generate(ThreadContext context, Session session, RubyString object, OutputStream buffer) throws IOException {
430431
try {
431432
object = ensureValidEncoding(context, object);
432-
StringEncoder stringEncoder = session.getStringEncoder(context);
433-
ByteList byteList = object.getByteList();
434-
switch (object.scanForCodeRange()) {
435-
case StringSupport.CR_7BIT:
436-
stringEncoder.encodeASCII(context, byteList, buffer);
437-
break;
438-
case StringSupport.CR_VALID:
439-
stringEncoder.encode(context, byteList, buffer);
440-
break;
441-
default:
442-
throw stringEncoder.invalidUtf8(context);
443-
}
444433
} catch (RaiseException re) {
445-
throw Utils.newException(context, Utils.M_GENERATOR_ERROR, re.getMessage());
434+
RubyException exc = Utils.buildGeneratorError(context, object, re.getMessage());
435+
exc.setCause(re.getException());
436+
throw exc.toThrowable();
437+
}
438+
439+
StringEncoder stringEncoder = session.getStringEncoder(context);
440+
ByteList byteList = object.getByteList();
441+
switch (object.scanForCodeRange()) {
442+
case StringSupport.CR_7BIT:
443+
stringEncoder.encodeASCII(context, byteList, buffer);
444+
break;
445+
case StringSupport.CR_VALID:
446+
stringEncoder.encode(context, byteList, buffer);
447+
break;
448+
default:
449+
throw Utils.buildGeneratorError(context, object, "source sequence is illegal/malformed utf-8").toThrowable();
446450
}
447451
}
448452
};
@@ -506,7 +510,7 @@ void generate(ThreadContext context, Session session, IRubyObject object, Output
506510
RubyString generateNew(ThreadContext context, Session session, IRubyObject object) {
507511
GeneratorState state = session.getState(context);
508512
if (state.strict()) {
509-
throw Utils.newException(context, Utils.M_GENERATOR_ERROR, object + " not allowed in JSON");
513+
throw Utils.buildGeneratorError(context, object, object + " not allowed in JSON").toThrowable();
510514
} else if (object.respondsTo("to_json")) {
511515
IRubyObject result = object.callMethod(context, "to_json", state);
512516
if (result instanceof RubyString) return (RubyString)result;

java/src/json/ext/StringEncoder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,6 @@ private void escapeCodeUnit(char c, int auxOffset) {
142142

143143
@Override
144144
protected RaiseException invalidUtf8(ThreadContext context) {
145-
return Utils.newException(context, Utils.M_GENERATOR_ERROR, "source sequence is illegal/malformed utf-8");
145+
return Utils.newException(context, Utils.M_GENERATOR_ERROR, "source sequence is illegal/malformed utf-8");
146146
}
147147
}

java/src/json/ext/Utils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ static RaiseException newException(ThreadContext context,
6363
return excptn.toThrowable();
6464
}
6565

66+
static RubyException buildGeneratorError(ThreadContext context, IRubyObject invalidObject, RubyString message) {
67+
RuntimeInfo info = RuntimeInfo.forRuntime(context.runtime);
68+
RubyClass klazz = info.jsonModule.get().getClass(M_GENERATOR_ERROR);
69+
RubyException excptn = (RubyException)klazz.newInstance(context, message, Block.NULL_BLOCK);
70+
excptn.setInstanceVariable("@invalid_object", invalidObject);
71+
return excptn;
72+
}
73+
74+
static RubyException buildGeneratorError(ThreadContext context, IRubyObject invalidObject, String message) {
75+
return buildGeneratorError(context, invalidObject, context.runtime.newString(message));
76+
}
77+
6678
static byte[] repeat(ByteList a, int n) {
6779
return repeat(a.unsafeBytes(), a.begin(), a.length(), n);
6880
}

lib/json/common.rb

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,23 @@ class CircularDatastructure < NestingError; end
143143
# :startdoc:
144144

145145
# This exception is raised if a generator or unparser error occurs.
146-
class GeneratorError < JSONError; end
146+
class GeneratorError < JSONError
147+
attr_reader :invalid_object
148+
149+
def initialize(message, invalid_object = nil)
150+
super(message)
151+
@invalid_object = invalid_object
152+
end
153+
154+
def detailed_message(...)
155+
if @invalid_object.nil?
156+
super
157+
else
158+
"#{super}\nInvalid object: #{@invalid_object.inspect}"
159+
end
160+
end
161+
end
162+
147163
# For backwards compatibility
148164
UnparserError = GeneratorError # :nodoc:
149165

lib/json/truffle_ruby/generator.rb

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ def utf8_to_json(string, script_safe = false) # :nodoc:
6262
string
6363
end
6464

65-
def utf8_to_json_ascii(string, script_safe = false) # :nodoc:
66-
string = string.b
65+
def utf8_to_json_ascii(original_string, script_safe = false) # :nodoc:
66+
string = original_string.b
6767
map = script_safe ? SCRIPT_SAFE_MAP : MAP
6868
string.gsub!(/[\/"\\\x0-\x1f]/n) { map[$&] || $& }
6969
string.gsub!(/(
@@ -74,7 +74,7 @@ def utf8_to_json_ascii(string, script_safe = false) # :nodoc:
7474
)+ |
7575
[\x80-\xc1\xf5-\xff] # invalid
7676
)/nx) { |c|
77-
c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'"
77+
c.size == 1 and raise GeneratorError.new("invalid utf8 byte: '#{c}'", original_string)
7878
s = c.encode(::Encoding::UTF_16BE, ::Encoding::UTF_8).unpack('H*')[0]
7979
s.force_encoding(::Encoding::BINARY)
8080
s.gsub!(/.{4}/n, '\\\\u\&')
@@ -83,7 +83,7 @@ def utf8_to_json_ascii(string, script_safe = false) # :nodoc:
8383
string.force_encoding(::Encoding::UTF_8)
8484
string
8585
rescue => e
86-
raise GeneratorError.wrap(e)
86+
raise GeneratorError.new(e.message, original_string)
8787
end
8888

8989
def valid_utf8?(string)
@@ -306,8 +306,10 @@ def generate(obj)
306306
else
307307
result = obj.to_json(self)
308308
end
309-
JSON::TruffleRuby::Generator.valid_utf8?(result) or raise GeneratorError,
310-
"source sequence #{result.inspect} is illegal/malformed utf-8"
309+
JSON::TruffleRuby::Generator.valid_utf8?(result) or raise GeneratorError.new(
310+
"source sequence #{result.inspect} is illegal/malformed utf-8",
311+
obj
312+
)
311313
result
312314
end
313315

@@ -364,10 +366,10 @@ def generate(obj)
364366
begin
365367
string = string.encode(::Encoding::UTF_8)
366368
rescue Encoding::UndefinedConversionError => error
367-
raise GeneratorError, error.message
369+
raise GeneratorError.new(error.message, string)
368370
end
369371
end
370-
raise GeneratorError, "source sequence is illegal/malformed utf-8" unless string.valid_encoding?
372+
raise GeneratorError.new("source sequence is illegal/malformed utf-8", string) unless string.valid_encoding?
371373

372374
if /["\\\x0-\x1f]/n.match?(string)
373375
buf << string.gsub(/["\\\x0-\x1f]/n, MAP)
@@ -403,7 +405,7 @@ module Object
403405
# special method #to_json was defined for some object.
404406
def to_json(state = nil, *)
405407
if state && State.from_state(state).strict?
406-
raise GeneratorError, "#{self.class} not allowed in JSON"
408+
raise GeneratorError.new("#{self.class} not allowed in JSON", self)
407409
else
408410
to_s.to_json
409411
end
@@ -454,7 +456,7 @@ def json_transform(state)
454456

455457
result = +"#{result}#{key_json}#{state.space_before}:#{state.space}"
456458
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
457-
raise GeneratorError, "#{value.class} not allowed in JSON"
459+
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
458460
elsif value.respond_to?(:to_json)
459461
result << value.to_json(state)
460462
else
@@ -507,7 +509,7 @@ def json_transform(state)
507509
result << delim unless first
508510
result << state.indent * depth if indent
509511
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
510-
raise GeneratorError, "#{value.class} not allowed in JSON"
512+
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
511513
elsif value.respond_to?(:to_json)
512514
result << value.to_json(state)
513515
else
@@ -536,13 +538,13 @@ def to_json(state = nil, *)
536538
if state.allow_nan?
537539
to_s
538540
else
539-
raise GeneratorError, "#{self} not allowed in JSON"
541+
raise GeneratorError.new("#{self} not allowed in JSON", self)
540542
end
541543
when nan?
542544
if state.allow_nan?
543545
to_s
544546
else
545-
raise GeneratorError, "#{self} not allowed in JSON"
547+
raise GeneratorError.new("#{self} not allowed in JSON", self)
546548
end
547549
else
548550
to_s
@@ -558,7 +560,7 @@ def to_json(state = nil, *args)
558560
state = State.from_state(state)
559561
if encoding == ::Encoding::UTF_8
560562
unless valid_encoding?
561-
raise GeneratorError, "source sequence is illegal/malformed utf-8"
563+
raise GeneratorError.new("source sequence is illegal/malformed utf-8", self)
562564
end
563565
string = self
564566
else
@@ -570,7 +572,7 @@ def to_json(state = nil, *args)
570572
%("#{JSON::TruffleRuby::Generator.utf8_to_json(string, state.script_safe)}")
571573
end
572574
rescue Encoding::UndefinedConversionError => error
573-
raise ::JSON::GeneratorError, error.message
575+
raise ::JSON::GeneratorError.new(error.message, self)
574576
end
575577

576578
# Module that holds the extending methods if, the String module is

test/json/json_generator_test.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,17 +250,20 @@ def test_fast_state
250250
end
251251

252252
def test_allow_nan
253-
assert_raise(GeneratorError) { generate([JSON::NaN]) }
253+
error = assert_raise(GeneratorError) { generate([JSON::NaN]) }
254+
assert_same JSON::NaN, error.invalid_object
254255
assert_equal '[NaN]', generate([JSON::NaN], :allow_nan => true)
255256
assert_raise(GeneratorError) { fast_generate([JSON::NaN]) }
256257
assert_raise(GeneratorError) { pretty_generate([JSON::NaN]) }
257258
assert_equal "[\n NaN\n]", pretty_generate([JSON::NaN], :allow_nan => true)
258-
assert_raise(GeneratorError) { generate([JSON::Infinity]) }
259+
error = assert_raise(GeneratorError) { generate([JSON::Infinity]) }
260+
assert_same JSON::Infinity, error.invalid_object
259261
assert_equal '[Infinity]', generate([JSON::Infinity], :allow_nan => true)
260262
assert_raise(GeneratorError) { fast_generate([JSON::Infinity]) }
261263
assert_raise(GeneratorError) { pretty_generate([JSON::Infinity]) }
262264
assert_equal "[\n Infinity\n]", pretty_generate([JSON::Infinity], :allow_nan => true)
263-
assert_raise(GeneratorError) { generate([JSON::MinusInfinity]) }
265+
error = assert_raise(GeneratorError) { generate([JSON::MinusInfinity]) }
266+
assert_same JSON::MinusInfinity, error.invalid_object
264267
assert_equal '[-Infinity]', generate([JSON::MinusInfinity], :allow_nan => true)
265268
assert_raise(GeneratorError) { fast_generate([JSON::MinusInfinity]) }
266269
assert_raise(GeneratorError) { pretty_generate([JSON::MinusInfinity]) }
@@ -487,9 +490,13 @@ def test_invalid_encoding_string
487490
["\x82\xAC\xEF".b].to_json
488491
end
489492

490-
assert_raise(JSON::GeneratorError) do
491-
{ foo: "\x82\xAC\xEF".b }.to_json
493+
badly_encoded = "\x82\xAC\xEF".b
494+
exception = assert_raise(JSON::GeneratorError) do
495+
{ foo: badly_encoded }.to_json
492496
end
497+
498+
assert_kind_of EncodingError, exception.cause
499+
assert_same badly_encoded, exception.invalid_object
493500
end
494501

495502
class MyCustomString < String

0 commit comments

Comments
 (0)