From f86b5954f4d4d2172b4dc73775177b4840aba22d Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 19 Feb 2026 13:20:18 +0100 Subject: [PATCH] Add `allow_invalid_escape` parsing option Ref: https://github.com/ruby/json/issues/939 --- CHANGES.md | 2 + ext/json/ext/parser/parser.c | 10 +- java/src/json/ext/ParserConfig.java | 150 ++++++++++++++------------- java/src/json/ext/ParserConfig.rl | 4 +- java/src/json/ext/StringDecoder.java | 13 ++- lib/json.rb | 12 +++ test/json/json_parser_test.rb | 8 ++ 7 files changed, 119 insertions(+), 80 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 14d9944c..ceb17198 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ ### Unreleased +* Add `allow_invalid_escape` parsing option to ignore backslashes that aren't followed by one of the valid escape characters. + ### 2026-02-03 (2.18.1) * Fix a potential crash in very specific circumstance if GC triggers during a call to `to_json` diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index e4b619b4..42ca1089 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -7,8 +7,9 @@ static VALUE CNaN, CInfinity, CMinusInfinity; static ID i_new, i_try_convert, i_uminus, i_encode; -static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_control_characters, sym_symbolize_names, sym_freeze, - sym_decimal_class, sym_on_load, sym_allow_duplicate_key; +static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_control_characters, + sym_allow_invalid_escape, sym_symbolize_names, sym_freeze, sym_decimal_class, sym_on_load, + sym_allow_duplicate_key; static int binary_encindex; static int utf8_encindex; @@ -336,6 +337,7 @@ typedef struct JSON_ParserStruct { bool allow_nan; bool allow_trailing_comma; bool allow_control_characters; + bool allow_invalid_escape; bool symbolize_names; bool freeze; } JSON_ParserConfig; @@ -746,6 +748,8 @@ NOINLINE(static) VALUE json_string_unescape(JSON_ParserState *state, JSON_Parser } raise_parse_error_at("invalid ASCII control character in string: %s", state, pe - 1); } + } else if (config->allow_invalid_escape) { + APPEND_CHAR(*pe); } else { raise_parse_error_at("invalid escape character in string: %s", state, pe - 1); } @@ -1435,6 +1439,7 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data) else if (key == sym_allow_nan) { config->allow_nan = RTEST(val); } else if (key == sym_allow_trailing_comma) { config->allow_trailing_comma = RTEST(val); } else if (key == sym_allow_control_characters) { config->allow_control_characters = RTEST(val); } + else if (key == sym_allow_invalid_escape) { config->allow_invalid_escape = RTEST(val); } else if (key == sym_symbolize_names) { config->symbolize_names = RTEST(val); } else if (key == sym_freeze) { config->freeze = RTEST(val); } else if (key == sym_on_load) { config->on_load_proc = RTEST(val) ? val : Qfalse; } @@ -1653,6 +1658,7 @@ void Init_parser(void) sym_allow_nan = ID2SYM(rb_intern("allow_nan")); sym_allow_trailing_comma = ID2SYM(rb_intern("allow_trailing_comma")); sym_allow_control_characters = ID2SYM(rb_intern("allow_control_characters")); + sym_allow_invalid_escape = ID2SYM(rb_intern("allow_invalid_escape")); sym_symbolize_names = ID2SYM(rb_intern("symbolize_names")); sym_freeze = ID2SYM(rb_intern("freeze")); sym_on_load = ID2SYM(rb_intern("on_load")); diff --git a/java/src/json/ext/ParserConfig.java b/java/src/json/ext/ParserConfig.java index 23231646..dfc1a6c2 100644 --- a/java/src/json/ext/ParserConfig.java +++ b/java/src/json/ext/ParserConfig.java @@ -55,6 +55,7 @@ public class ParserConfig extends RubyObject { private boolean allowNaN; private boolean allowTrailingComma; private boolean allowControlCharacters; + private boolean allowInvalidEscape; private boolean allowDuplicateKey; private boolean deprecateDuplicateKey; private boolean symbolizeNames; @@ -180,6 +181,7 @@ public IRubyObject initialize(ThreadContext context, IRubyObject options) { this.maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING); this.allowNaN = opts.getBool("allow_nan", false); this.allowControlCharacters = opts.getBool("allow_control_characters", false); + this.allowInvalidEscape = opts.getBool("allow_invalid_escape", false); this.allowTrailingComma = opts.getBool("allow_trailing_comma", false); this.symbolizeNames = opts.getBool("symbolize_names", false); if (opts.hasKey("allow_duplicate_key")) { @@ -290,7 +292,7 @@ private ParserSession(ParserConfig config, RubyString source, ThreadContext cont this.byteList = source.getByteList(); this.data = byteList.unsafeBytes(); this.view = new ByteList(data, false); - this.decoder = new StringDecoder(config.allowControlCharacters); + this.decoder = new StringDecoder(config.allowControlCharacters, config.allowInvalidEscape); } private RaiseException parsingError(ThreadContext context, String message, int absStart, int absEnd) { @@ -305,11 +307,11 @@ private RaiseException unexpectedToken(ThreadContext context, int absStart, int } -// line 331 "ParserConfig.rl" +// line 333 "ParserConfig.rl" -// line 313 "ParserConfig.java" +// line 315 "ParserConfig.java" private static byte[] init__JSON_value_actions_0() { return new byte [] { @@ -423,7 +425,7 @@ private static byte[] init__JSON_value_from_state_actions_0() static final int JSON_value_en_main = 1; -// line 437 "ParserConfig.rl" +// line 439 "ParserConfig.rl" void parseValue(ThreadContext context, ParserResult res, int p, int pe) { @@ -431,14 +433,14 @@ void parseValue(ThreadContext context, ParserResult res, int p, int pe) { IRubyObject result = null; -// line 435 "ParserConfig.java" +// line 437 "ParserConfig.java" { cs = JSON_value_start; } -// line 444 "ParserConfig.rl" +// line 446 "ParserConfig.rl" -// line 442 "ParserConfig.java" +// line 444 "ParserConfig.java" { int _klen; int _trans = 0; @@ -464,13 +466,13 @@ void parseValue(ThreadContext context, ParserResult res, int p, int pe) { while ( _nacts-- > 0 ) { switch ( _JSON_value_actions[_acts++] ) { case 9: -// line 422 "ParserConfig.rl" +// line 424 "ParserConfig.rl" { p--; { p += 1; _goto_targ = 5; if (true) continue _goto;} } break; -// line 474 "ParserConfig.java" +// line 476 "ParserConfig.java" } } @@ -533,25 +535,25 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) switch ( _JSON_value_actions[_acts++] ) { case 0: -// line 339 "ParserConfig.rl" +// line 341 "ParserConfig.rl" { result = context.nil; } break; case 1: -// line 342 "ParserConfig.rl" +// line 344 "ParserConfig.rl" { result = context.fals; } break; case 2: -// line 345 "ParserConfig.rl" +// line 347 "ParserConfig.rl" { result = context.tru; } break; case 3: -// line 348 "ParserConfig.rl" +// line 350 "ParserConfig.rl" { if (config.allowNaN) { result = getConstant(CONST_NAN); @@ -561,7 +563,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) } break; case 4: -// line 355 "ParserConfig.rl" +// line 357 "ParserConfig.rl" { if (config.allowNaN) { result = getConstant(CONST_INFINITY); @@ -571,7 +573,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) } break; case 5: -// line 362 "ParserConfig.rl" +// line 364 "ParserConfig.rl" { if (pe > p + 8 && absSubSequence(p, p + 9).equals(JSON_MINUS_INFINITY)) { @@ -600,7 +602,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) } break; case 6: -// line 388 "ParserConfig.rl" +// line 390 "ParserConfig.rl" { parseString(context, res, p, pe); if (res.result == null) { @@ -613,7 +615,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) } break; case 7: -// line 398 "ParserConfig.rl" +// line 400 "ParserConfig.rl" { currentNesting++; parseArray(context, res, p, pe); @@ -628,7 +630,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) } break; case 8: -// line 410 "ParserConfig.rl" +// line 412 "ParserConfig.rl" { currentNesting++; parseObject(context, res, p, pe); @@ -642,7 +644,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) } } break; -// line 646 "ParserConfig.java" +// line 648 "ParserConfig.java" } } } @@ -662,7 +664,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) break; } } -// line 445 "ParserConfig.rl" +// line 447 "ParserConfig.rl" if (cs >= JSON_value_first_final && result != null) { if (config.freeze) { @@ -675,7 +677,7 @@ else if ( data[p] > _JSON_value_trans_keys[_mid+1] ) } -// line 679 "ParserConfig.java" +// line 681 "ParserConfig.java" private static byte[] init__JSON_integer_actions_0() { return new byte [] { @@ -774,7 +776,7 @@ private static byte[] init__JSON_integer_trans_actions_0() static final int JSON_integer_en_main = 1; -// line 467 "ParserConfig.rl" +// line 469 "ParserConfig.rl" void parseInteger(ThreadContext context, ParserResult res, int p, int pe) { @@ -791,15 +793,15 @@ int parseIntegerInternal(int p, int pe) { int cs; -// line 795 "ParserConfig.java" +// line 797 "ParserConfig.java" { cs = JSON_integer_start; } -// line 483 "ParserConfig.rl" +// line 485 "ParserConfig.rl" int memo = p; -// line 803 "ParserConfig.java" +// line 805 "ParserConfig.java" { int _klen; int _trans = 0; @@ -880,13 +882,13 @@ else if ( data[p] > _JSON_integer_trans_keys[_mid+1] ) switch ( _JSON_integer_actions[_acts++] ) { case 0: -// line 461 "ParserConfig.rl" +// line 463 "ParserConfig.rl" { p--; { p += 1; _goto_targ = 5; if (true) continue _goto;} } break; -// line 890 "ParserConfig.java" +// line 892 "ParserConfig.java" } } } @@ -906,7 +908,7 @@ else if ( data[p] > _JSON_integer_trans_keys[_mid+1] ) break; } } -// line 485 "ParserConfig.rl" +// line 487 "ParserConfig.rl" if (cs < JSON_integer_first_final) { return -1; @@ -926,7 +928,7 @@ RubyInteger bytesToInum(Ruby runtime, ByteList num) { } -// line 930 "ParserConfig.java" +// line 932 "ParserConfig.java" private static byte[] init__JSON_float_actions_0() { return new byte [] { @@ -1028,7 +1030,7 @@ private static byte[] init__JSON_float_trans_actions_0() static final int JSON_float_en_main = 1; -// line 518 "ParserConfig.rl" +// line 520 "ParserConfig.rl" void parseFloat(ThreadContext context, ParserResult res, int p, int pe) { @@ -1047,15 +1049,15 @@ int parseFloatInternal(int p, int pe) { int cs; -// line 1051 "ParserConfig.java" +// line 1053 "ParserConfig.java" { cs = JSON_float_start; } -// line 536 "ParserConfig.rl" +// line 538 "ParserConfig.rl" int memo = p; -// line 1059 "ParserConfig.java" +// line 1061 "ParserConfig.java" { int _klen; int _trans = 0; @@ -1136,13 +1138,13 @@ else if ( data[p] > _JSON_float_trans_keys[_mid+1] ) switch ( _JSON_float_actions[_acts++] ) { case 0: -// line 509 "ParserConfig.rl" +// line 511 "ParserConfig.rl" { p--; { p += 1; _goto_targ = 5; if (true) continue _goto;} } break; -// line 1146 "ParserConfig.java" +// line 1148 "ParserConfig.java" } } } @@ -1162,7 +1164,7 @@ else if ( data[p] > _JSON_float_trans_keys[_mid+1] ) break; } } -// line 538 "ParserConfig.rl" +// line 540 "ParserConfig.rl" if (cs < JSON_float_first_final) { return -1; @@ -1172,7 +1174,7 @@ else if ( data[p] > _JSON_float_trans_keys[_mid+1] ) } -// line 1176 "ParserConfig.java" +// line 1178 "ParserConfig.java" private static byte[] init__JSON_string_actions_0() { return new byte [] { @@ -1274,7 +1276,7 @@ private static byte[] init__JSON_string_trans_actions_0() static final int JSON_string_en_main = 1; -// line 577 "ParserConfig.rl" +// line 579 "ParserConfig.rl" void parseString(ThreadContext context, ParserResult res, int p, int pe) { @@ -1282,15 +1284,15 @@ void parseString(ThreadContext context, ParserResult res, int p, int pe) { IRubyObject result = null; -// line 1286 "ParserConfig.java" +// line 1288 "ParserConfig.java" { cs = JSON_string_start; } -// line 584 "ParserConfig.rl" +// line 586 "ParserConfig.rl" int memo = p; -// line 1294 "ParserConfig.java" +// line 1296 "ParserConfig.java" { int _klen; int _trans = 0; @@ -1371,7 +1373,7 @@ else if ( data[p] > _JSON_string_trans_keys[_mid+1] ) switch ( _JSON_string_actions[_acts++] ) { case 0: -// line 552 "ParserConfig.rl" +// line 554 "ParserConfig.rl" { int offset = byteList.begin(); ByteList decoded = decoder.decode(context, byteList, memo + 1 - offset, @@ -1386,13 +1388,13 @@ else if ( data[p] > _JSON_string_trans_keys[_mid+1] ) } break; case 1: -// line 565 "ParserConfig.rl" +// line 567 "ParserConfig.rl" { p--; { p += 1; _goto_targ = 5; if (true) continue _goto;} } break; -// line 1396 "ParserConfig.java" +// line 1398 "ParserConfig.java" } } } @@ -1412,7 +1414,7 @@ else if ( data[p] > _JSON_string_trans_keys[_mid+1] ) break; } } -// line 586 "ParserConfig.rl" +// line 588 "ParserConfig.rl" if (cs >= JSON_string_first_final && result != null) { if (result instanceof RubyString) { @@ -1433,7 +1435,7 @@ else if ( data[p] > _JSON_string_trans_keys[_mid+1] ) } -// line 1437 "ParserConfig.java" +// line 1439 "ParserConfig.java" private static byte[] init__JSON_array_actions_0() { return new byte [] { @@ -1600,7 +1602,7 @@ private static byte[] init__JSON_array_trans_actions_0() static final int JSON_array_en_main = 1; -// line 640 "ParserConfig.rl" +// line 642 "ParserConfig.rl" void parseArray(ThreadContext context, ParserResult res, int p, int pe) { @@ -1614,14 +1616,14 @@ void parseArray(ThreadContext context, ParserResult res, int p, int pe) { IRubyObject result = RubyArray.newArray(context.runtime); -// line 1618 "ParserConfig.java" +// line 1620 "ParserConfig.java" { cs = JSON_array_start; } -// line 653 "ParserConfig.rl" +// line 655 "ParserConfig.rl" -// line 1625 "ParserConfig.java" +// line 1627 "ParserConfig.java" { int _klen; int _trans = 0; @@ -1664,7 +1666,7 @@ else if ( _widec > _JSON_array_cond_keys[_mid+1] ) case 0: { _widec = 65536 + (data[p] - 0); if ( -// line 611 "ParserConfig.rl" +// line 613 "ParserConfig.rl" config.allowTrailingComma ) _widec += 65536; break; } @@ -1734,7 +1736,7 @@ else if ( _widec > _JSON_array_trans_keys[_mid+1] ) switch ( _JSON_array_actions[_acts++] ) { case 0: -// line 613 "ParserConfig.rl" +// line 615 "ParserConfig.rl" { parseValue(context, res, p, pe); if (res.result == null) { @@ -1747,13 +1749,13 @@ else if ( _widec > _JSON_array_trans_keys[_mid+1] ) } break; case 1: -// line 624 "ParserConfig.rl" +// line 626 "ParserConfig.rl" { p--; { p += 1; _goto_targ = 5; if (true) continue _goto;} } break; -// line 1757 "ParserConfig.java" +// line 1759 "ParserConfig.java" } } } @@ -1773,7 +1775,7 @@ else if ( _widec > _JSON_array_trans_keys[_mid+1] ) break; } } -// line 654 "ParserConfig.rl" +// line 656 "ParserConfig.rl" if (cs >= JSON_array_first_final) { res.update(config.onLoad(context, result), p + 1); @@ -1783,7 +1785,7 @@ else if ( _widec > _JSON_array_trans_keys[_mid+1] ) } -// line 1787 "ParserConfig.java" +// line 1789 "ParserConfig.java" private static byte[] init__JSON_object_actions_0() { return new byte [] { @@ -1960,7 +1962,7 @@ private static byte[] init__JSON_object_trans_actions_0() static final int JSON_object_en_main = 1; -// line 724 "ParserConfig.rl" +// line 726 "ParserConfig.rl" void parseObject(ThreadContext context, ParserResult res, int p, int pe) { @@ -1977,14 +1979,14 @@ void parseObject(ThreadContext context, ParserResult res, int p, int pe) { IRubyObject result = RubyHash.newHash(context.runtime); -// line 1981 "ParserConfig.java" +// line 1983 "ParserConfig.java" { cs = JSON_object_start; } -// line 740 "ParserConfig.rl" +// line 742 "ParserConfig.rl" -// line 1988 "ParserConfig.java" +// line 1990 "ParserConfig.java" { int _klen; int _trans = 0; @@ -2027,7 +2029,7 @@ else if ( _widec > _JSON_object_cond_keys[_mid+1] ) case 0: { _widec = 65536 + (data[p] - 0); if ( -// line 668 "ParserConfig.rl" +// line 670 "ParserConfig.rl" config.allowTrailingComma ) _widec += 65536; break; } @@ -2097,7 +2099,7 @@ else if ( _widec > _JSON_object_trans_keys[_mid+1] ) switch ( _JSON_object_actions[_acts++] ) { case 0: -// line 670 "ParserConfig.rl" +// line 672 "ParserConfig.rl" { parseValue(context, res, p, pe); if (res.result == null) { @@ -2110,7 +2112,7 @@ else if ( _widec > _JSON_object_trans_keys[_mid+1] ) } break; case 1: -// line 681 "ParserConfig.rl" +// line 683 "ParserConfig.rl" { parseString(context, res, p, pe); if (res.result == null) { @@ -2141,13 +2143,13 @@ else if ( _widec > _JSON_object_trans_keys[_mid+1] ) } break; case 2: -// line 710 "ParserConfig.rl" +// line 712 "ParserConfig.rl" { p--; { p += 1; _goto_targ = 5; if (true) continue _goto;} } break; -// line 2151 "ParserConfig.java" +// line 2153 "ParserConfig.java" } } } @@ -2167,7 +2169,7 @@ else if ( _widec > _JSON_object_trans_keys[_mid+1] ) break; } } -// line 741 "ParserConfig.rl" +// line 743 "ParserConfig.rl" if (cs < JSON_object_first_final) { res.update(null, p + 1); @@ -2178,7 +2180,7 @@ else if ( _widec > _JSON_object_trans_keys[_mid+1] ) } -// line 2182 "ParserConfig.java" +// line 2184 "ParserConfig.java" private static byte[] init__JSON_actions_0() { return new byte [] { @@ -2281,7 +2283,7 @@ private static byte[] init__JSON_trans_actions_0() static final int JSON_en_main = 1; -// line 770 "ParserConfig.rl" +// line 772 "ParserConfig.rl" public IRubyObject parseImplementation(ThreadContext context) { @@ -2291,16 +2293,16 @@ public IRubyObject parseImplementation(ThreadContext context) { ParserResult res = new ParserResult(); -// line 2295 "ParserConfig.java" +// line 2297 "ParserConfig.java" { cs = JSON_start; } -// line 779 "ParserConfig.rl" +// line 781 "ParserConfig.rl" p = byteList.begin(); pe = p + byteList.length(); -// line 2304 "ParserConfig.java" +// line 2306 "ParserConfig.java" { int _klen; int _trans = 0; @@ -2381,7 +2383,7 @@ else if ( data[p] > _JSON_trans_keys[_mid+1] ) switch ( _JSON_actions[_acts++] ) { case 0: -// line 756 "ParserConfig.rl" +// line 758 "ParserConfig.rl" { parseValue(context, res, p, pe); if (res.result == null) { @@ -2393,7 +2395,7 @@ else if ( data[p] > _JSON_trans_keys[_mid+1] ) } } break; -// line 2397 "ParserConfig.java" +// line 2399 "ParserConfig.java" } } } @@ -2413,7 +2415,7 @@ else if ( data[p] > _JSON_trans_keys[_mid+1] ) break; } } -// line 782 "ParserConfig.rl" +// line 784 "ParserConfig.rl" if (cs >= JSON_first_final && p == pe) { return result; diff --git a/java/src/json/ext/ParserConfig.rl b/java/src/json/ext/ParserConfig.rl index a1c9ea7f..53678ec9 100644 --- a/java/src/json/ext/ParserConfig.rl +++ b/java/src/json/ext/ParserConfig.rl @@ -53,6 +53,7 @@ public class ParserConfig extends RubyObject { private boolean allowNaN; private boolean allowTrailingComma; private boolean allowControlCharacters; + private boolean allowInvalidEscape; private boolean allowDuplicateKey; private boolean deprecateDuplicateKey; private boolean symbolizeNames; @@ -178,6 +179,7 @@ public class ParserConfig extends RubyObject { this.maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING); this.allowNaN = opts.getBool("allow_nan", false); this.allowControlCharacters = opts.getBool("allow_control_characters", false); + this.allowInvalidEscape = opts.getBool("allow_invalid_escape", false); this.allowTrailingComma = opts.getBool("allow_trailing_comma", false); this.symbolizeNames = opts.getBool("symbolize_names", false); if (opts.hasKey("allow_duplicate_key")) { @@ -288,7 +290,7 @@ public class ParserConfig extends RubyObject { this.byteList = source.getByteList(); this.data = byteList.unsafeBytes(); this.view = new ByteList(data, false); - this.decoder = new StringDecoder(config.allowControlCharacters); + this.decoder = new StringDecoder(config.allowControlCharacters, config.allowInvalidEscape); } private RaiseException parsingError(ThreadContext context, String message, int absStart, int absEnd) { diff --git a/java/src/json/ext/StringDecoder.java b/java/src/json/ext/StringDecoder.java index a588d94d..091b09f5 100644 --- a/java/src/json/ext/StringDecoder.java +++ b/java/src/json/ext/StringDecoder.java @@ -23,14 +23,16 @@ final class StringDecoder extends ByteListTranscoder { */ private int surrogatePairStart = -1; private boolean allowControlCharacters = false; + private boolean allowInvalidEscape = false; private ByteList out; // Array used for writing multibyte characters into the buffer at once private final byte[] aux = new byte[4]; - public StringDecoder(boolean allowControlCharacters) { + public StringDecoder(boolean allowControlCharacters, boolean allowInvalidEscape) { this.allowControlCharacters = allowControlCharacters; + this.allowInvalidEscape = allowInvalidEscape; } ByteList decode(ThreadContext context, ByteList src, int start, int end) { @@ -67,7 +69,8 @@ private void handleChar(ThreadContext context, int c) throws IOException { private void handleEscapeSequence(ThreadContext context) throws IOException { ensureMin(context, 1); - switch (readUtf8Char(context)) { + int character = readUtf8Char(context); + switch (character) { case 'b': append('\b'); break; @@ -105,7 +108,11 @@ private void handleEscapeSequence(ThreadContext context) throws IOException { } break; default: - throw invalidEscape(context); + if (allowInvalidEscape) { + append(character); + } else { + throw invalidEscape(context); + } } } diff --git a/lib/json.rb b/lib/json.rb index 2f6db442..39e414be 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -194,6 +194,18 @@ # When enabled: # JSON.parse(%{"Hello\nWorld"}, allow_control_characters: true) # => "Hello\nWorld" # +# --- +# +# Option +allow_invalid_escape+ (boolean) specifies whether to ignore backslahes that are followed +# by an invalid escape character in strings; +# defaults to +false+. +# +# With the default, +false+: +# JSON.parse(%{"Hell\\o"}) # invalid escape character in string (JSON::ParserError) +# +# When enabled: +# JSON.parse(%{"Hell\\o"}, allow_invalid_escape: true) # => "Hello" +# # ====== Output Options # # Option +freeze+ (boolean) specifies whether the returned objects will be frozen; diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 54a6bbbd..653abf46 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -183,6 +183,14 @@ def test_parse_allowed_control_chars_in_string end end + def test_parse_invalid_escape + assert_raise JSON::ParserError do + parse(%("fo\\o")) + end + + assert_equal "foo", parse(%("fo\\o"), allow_invalid_escape: true) + end + def test_parse_arrays assert_equal([1,2,3], parse('[1,2,3]')) assert_equal([1.2,2,3], parse('[1.2,2,3]'))