From 8a18cf6a14fc1fae54b3d581434a255c9d98ceea Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 30 Jan 2020 12:48:05 +0100 Subject: Add an option to escape forward slash character Squashed commit of the following: commit 26d181059989279a79c433cedcd893b4f52e42ee Author: Francois Chagnon Date: Tue Sep 15 21:17:34 2015 +0000 add config options for escape_slash commit fa282334051b16df91ca097dd7304b46f3bc7719 Author: Francois Chagnon Date: Mon Feb 9 21:09:33 2015 +0000 add forward slash to escape character --- ext/json/ext/generator/generator.c | 53 +++++++++++++++++++++++++++++++---- ext/json/ext/generator/generator.h | 7 +++-- java/src/json/ext/Generator.java | 2 +- java/src/json/ext/GeneratorState.java | 30 ++++++++++++++++++++ java/src/json/ext/StringEncoder.java | 10 +++++-- lib/json/common.rb | 3 +- lib/json/pure/generator.rb | 30 ++++++++++++++++---- tests/json_generator_test.rb | 7 +++++ tests/json_parser_test.rb | 4 +++ 9 files changed, 129 insertions(+), 17 deletions(-) mode change 100644 => 100755 tests/json_generator_test.rb diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 749efaf..80d1ca7 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -22,7 +22,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_buffer_initial_length, i_dup, i_escape_slash; /* * Copyright 2001-2004 Unicode, Inc. @@ -130,7 +130,7 @@ static void unicode_escape_to_buffer(FBuffer *buffer, char buf[6], UTF16 /* Converts string to a JSON string in FBuffer buffer, where all but the ASCII * and control characters are JSON escaped. */ -static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string) +static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string, char escape_slash) { const UTF8 *source = (UTF8 *) RSTRING_PTR(string); const UTF8 *sourceEnd = source + RSTRING_LEN(string); @@ -180,6 +180,11 @@ static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string) case '"': fbuffer_append(buffer, "\\\"", 2); break; + case '/': + if(escape_slash) { + fbuffer_append(buffer, "\\/", 2); + break; + } default: fbuffer_append_char(buffer, (char)ch); break; @@ -229,7 +234,7 @@ static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string) * characters required by the JSON standard are JSON escaped. The remaining * characters (should be UTF8) are just passed through and appended to the * result. */ -static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string) +static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string, char escape_slash) { const char *ptr = RSTRING_PTR(string), *p; unsigned long len = RSTRING_LEN(string), start = 0, end = 0; @@ -280,6 +285,12 @@ static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string) escape = "\\\""; escape_len = 2; break; + case '/': + if(escape_slash) { + escape = "\\/"; + escape_len = 2; + break; + } default: { unsigned short clen = 1; @@ -716,6 +727,8 @@ static VALUE cState_configure(VALUE self, VALUE opts) state->allow_nan = RTEST(tmp); tmp = rb_hash_aref(opts, ID2SYM(i_ascii_only)); state->ascii_only = RTEST(tmp); + tmp = rb_hash_aref(opts, ID2SYM(i_escape_slash)); + state->escape_slash = RTEST(tmp); return self; } @@ -750,6 +763,7 @@ static VALUE cState_to_h(VALUE self) rb_hash_aset(result, ID2SYM(i_allow_nan), state->allow_nan ? Qtrue : Qfalse); 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_escape_slash), state->escape_slash ? 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; @@ -934,9 +948,9 @@ static void generate_json_string(FBuffer *buffer, VALUE Vstate, JSON_Generator_S } #endif if (state->ascii_only) { - convert_UTF8_to_JSON_ASCII(buffer, obj); + convert_UTF8_to_JSON_ASCII(buffer, obj, state->escape_slash); } else { - convert_UTF8_to_JSON(buffer, obj); + convert_UTF8_to_JSON(buffer, obj, state->escape_slash); } fbuffer_append_char(buffer, '"'); } @@ -1377,6 +1391,31 @@ static VALUE cState_max_nesting_set(VALUE self, VALUE depth) return state->max_nesting = FIX2LONG(depth); } +/* + * call-seq: escape_slash + * + * If this boolean is true, the forward slashes will be escaped in + * the json output. + */ +static VALUE cState_escape_slash(VALUE self) +{ + GET_STATE(self); + return state->escape_slash ? Qtrue : Qfalse; +} + +/* + * call-seq: escape_slash=(depth) + * + * This sets whether or not the forward slashes will be escaped in + * the json output. + */ +static VALUE cState_escape_slash_set(VALUE self, VALUE enable) +{ + GET_STATE(self); + state->escape_slash = RTEST(enable); + return Qnil; +} + /* * call-seq: allow_nan? * @@ -1489,6 +1528,9 @@ void Init_generator(void) rb_define_method(cState, "array_nl=", cState_array_nl_set, 1); rb_define_method(cState, "max_nesting", cState_max_nesting, 0); rb_define_method(cState, "max_nesting=", cState_max_nesting_set, 1); + rb_define_method(cState, "escape_slash", cState_escape_slash, 0); + rb_define_method(cState, "escape_slash?", cState_escape_slash, 0); + rb_define_method(cState, "escape_slash=", cState_escape_slash_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); @@ -1545,6 +1587,7 @@ void Init_generator(void) i_object_nl = rb_intern("object_nl"); i_array_nl = rb_intern("array_nl"); i_max_nesting = rb_intern("max_nesting"); + i_escape_slash = rb_intern("escape_slash"); i_allow_nan = rb_intern("allow_nan"); i_ascii_only = rb_intern("ascii_only"); i_depth = rb_intern("depth"); diff --git a/ext/json/ext/generator/generator.h b/ext/json/ext/generator/generator.h index c367a62..3ebd622 100644 --- a/ext/json/ext/generator/generator.h +++ b/ext/json/ext/generator/generator.h @@ -49,8 +49,8 @@ static const UTF32 halfMask = 0x3FFUL; static unsigned char isLegalUTF8(const UTF8 *source, unsigned long length); static void unicode_escape(char *buf, UTF16 character); static void unicode_escape_to_buffer(FBuffer *buffer, char buf[6], UTF16 character); -static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string); -static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string); +static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string, char escape_slash); +static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string, char escape_slash); static char *fstrndup(const char *ptr, unsigned long len); /* ruby api and some helpers */ @@ -72,6 +72,7 @@ typedef struct JSON_Generator_StateStruct { long max_nesting; char allow_nan; char ascii_only; + char escape_slash; long depth; long buffer_initial_length; } JSON_Generator_State; @@ -150,6 +151,8 @@ static VALUE cState_allow_nan_p(VALUE self); static VALUE cState_ascii_only_p(VALUE self); static VALUE cState_depth(VALUE self); static VALUE cState_depth_set(VALUE self, VALUE depth); +static VALUE cState_escape_slash(VALUE self); +static VALUE cState_escape_slash_set(VALUE self, VALUE depth); static FBuffer *cState_prepare_buffer(VALUE self); #ifndef ZALLOC #define ZALLOC(type) ((type *)ruby_zalloc(sizeof(type))) diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index 24bf049..cd23c73 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -158,7 +158,7 @@ public final class Generator { public StringEncoder getStringEncoder() { if (stringEncoder == null) { - stringEncoder = new StringEncoder(context, getState().asciiOnly()); + stringEncoder = new StringEncoder(context, getState().asciiOnly(), getState().escapeSlash()); } return stringEncoder; } diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 44a9311..7fec09d 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -82,6 +82,12 @@ public class GeneratorState extends RubyObject { */ private boolean quirksMode = DEFAULT_QUIRKS_MODE; static final boolean DEFAULT_QUIRKS_MODE = false; + /** + * If set to true the forward slash will be escaped in + * json output. + */ + private boolean escapeSlash = DEFAULT_ESCAPE_SLASH; + static final boolean DEFAULT_ESCAPE_SLASH = false; /** * The initial buffer length of this state. (This isn't really used on all * non-C implementations.) @@ -171,6 +177,9 @@ public class GeneratorState extends RubyObject { * -Infinity should be generated, otherwise an exception is * thrown if these values are encountered. * This options defaults to false. + *
:escape_slash + *
set to true if the forward slashes should be escaped + * in the json output (default: false) */ @JRubyMethod(optional=1, visibility=Visibility.PRIVATE) public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { @@ -194,6 +203,7 @@ public class GeneratorState extends RubyObject { this.allowNaN = orig.allowNaN; this.asciiOnly = orig.asciiOnly; this.quirksMode = orig.quirksMode; + this.escapeSlash = orig.escapeSlash; this.bufferInitialLength = orig.bufferInitialLength; this.depth = orig.depth; return this; @@ -346,6 +356,24 @@ public class GeneratorState extends RubyObject { return max_nesting; } + /** + * Returns true if forward slashes are escaped in the json output. + */ + public boolean escapeSlash() { + return escapeSlash; + } + + @JRubyMethod(name="escape_slash") + public RubyBoolean escape_slash_get(ThreadContext context) { + return context.getRuntime().newBoolean(escapeSlash); + } + + @JRubyMethod(name="escape_slash=") + public IRubyObject escape_slash_set(IRubyObject escape_slash) { + escapeSlash = escape_slash.isTrue(); + return escape_slash.getRuntime().newBoolean(escapeSlash); + } + public boolean allowNaN() { return allowNaN; } @@ -430,6 +458,7 @@ public class GeneratorState extends RubyObject { maxNesting = opts.getInt("max_nesting", DEFAULT_MAX_NESTING); allowNaN = opts.getBool("allow_nan", DEFAULT_ALLOW_NAN); asciiOnly = opts.getBool("ascii_only", DEFAULT_ASCII_ONLY); + escapeSlash = opts.getBool("escape_slash", DEFAULT_ESCAPE_SLASH); bufferInitialLength = opts.getInt("buffer_initial_length", DEFAULT_BUFFER_INITIAL_LENGTH); depth = opts.getInt("depth", 0); @@ -457,6 +486,7 @@ public class GeneratorState extends RubyObject { result.op_aset(context, runtime.newSymbol("allow_nan"), allow_nan_p(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("escape_slash"), escape_slash_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()) { diff --git a/java/src/json/ext/StringEncoder.java b/java/src/json/ext/StringEncoder.java index 9d40dd3..26678ed 100644 --- a/java/src/json/ext/StringEncoder.java +++ b/java/src/json/ext/StringEncoder.java @@ -15,7 +15,7 @@ import org.jruby.util.ByteList; * and throws a GeneratorError if any problem is found. */ final class StringEncoder extends ByteListTranscoder { - private final boolean asciiOnly; + private final boolean asciiOnly, escapeSlash; // Escaped characters will reuse this array, to avoid new allocations // or appending them byte-by-byte @@ -37,9 +37,10 @@ final class StringEncoder extends ByteListTranscoder { new byte[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - StringEncoder(ThreadContext context, boolean asciiOnly) { + StringEncoder(ThreadContext context, boolean asciiOnly, boolean escapeSlash) { super(context); this.asciiOnly = asciiOnly; + this.escapeSlash = escapeSlash; } void encode(ByteList src, ByteList out) { @@ -73,6 +74,11 @@ final class StringEncoder extends ByteListTranscoder { case '\b': escapeChar('b'); break; + case '/': + if(escapeSlash) { + escapeChar((char)c); + break; + } default: if (c >= 0x20 && c <= 0x7f || (c >= 0x80 && !asciiOnly)) { diff --git a/lib/json/common.rb b/lib/json/common.rb index 132e9c8..ed4e047 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -436,12 +436,13 @@ module JSON # Sets or returns the default options for the JSON.dump method. # Initially: # opts = JSON.dump_default_options - # opts # => {:max_nesting=>false, :allow_nan=>true} + # opts # => {:max_nesting=>false, :allow_nan=>true, :escape_slash=>false} attr_accessor :dump_default_options end self.dump_default_options = { :max_nesting => false, :allow_nan => true, + :escape_slash => false, } # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns diff --git a/lib/json/pure/generator.rb b/lib/json/pure/generator.rb index 471af63..ef1c2e8 100644 --- a/lib/json/pure/generator.rb +++ b/lib/json/pure/generator.rb @@ -37,20 +37,26 @@ module JSON '\\' => '\\\\', } # :nodoc: + ESCAPE_SLASH_MAP = MAP.merge( + '/' => '\\/', + ) + # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with # UTF16 big endian characters as \u????, and return it. - def utf8_to_json(string) # :nodoc: + def utf8_to_json(string, escape_slash = false) # :nodoc: string = string.dup string.force_encoding(::Encoding::ASCII_8BIT) - string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] } + map = escape_slash ? ESCAPE_SLASH_MAP : MAP + string.gsub!(/[\/"\\\x0-\x1f]/) { map[$&] || $& } string.force_encoding(::Encoding::UTF_8) string end - def utf8_to_json_ascii(string) # :nodoc: + def utf8_to_json_ascii(string, escape_slash = false) # :nodoc: string = string.dup string.force_encoding(::Encoding::ASCII_8BIT) - string.gsub!(/["\\\x0-\x1f]/n) { MAP[$&] } + map = escape_slash ? ESCAPE_SLASH_MAP : MAP + string.gsub!(/[\/"\\\x0-\x1f]/n) { map[$&] || $& } string.gsub!(/( (?: [\xc2-\xdf][\x80-\xbf] | @@ -109,6 +115,7 @@ module JSON # * *space_before*: a string that is put before a : pair delimiter (default: ''), # * *object_nl*: a string that is put at the end of a JSON object (default: ''), # * *array_nl*: a string that is put at the end of a JSON array (default: ''), + # * *escape_slash*: true if forward slash (/) should be escaped (default: false) # * *check_circular*: is deprecated now, use the :max_nesting option instead, # * *max_nesting*: sets the maximum level of data structure nesting in # the generated JSON, max_nesting = 0 if no maximum should be checked. @@ -123,6 +130,7 @@ module JSON @array_nl = '' @allow_nan = false @ascii_only = false + @escape_slash = false @buffer_initial_length = 1024 configure opts end @@ -148,6 +156,10 @@ module JSON # the generated JSON, max_nesting = 0 if no maximum is checked. attr_accessor :max_nesting + # If this attribute is set to true, forward slashes will be escaped in + # all json strings. + attr_accessor :escape_slash + # :stopdoc: attr_reader :buffer_initial_length @@ -187,6 +199,11 @@ module JSON @ascii_only end + # Returns true, if forward slashes are escaped. Otherwise returns false. + def escape_slash? + @escape_slash + end + # Configure this State instance with the Hash _opts_, and return # itself. def configure(opts) @@ -209,6 +226,7 @@ module JSON @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only) @depth = opts[:depth] || 0 @buffer_initial_length ||= opts[:buffer_initial_length] + @escape_slash = !!opts[:escape_slash] if opts.key?(:escape_slash) if !opts.key?(:max_nesting) # defaults to 100 @max_nesting = 100 @@ -399,9 +417,9 @@ module JSON string = encode(::Encoding::UTF_8) end if state.ascii_only? - '"' << JSON.utf8_to_json_ascii(string) << '"' + '"' << JSON.utf8_to_json_ascii(string, state.escape_slash) << '"' else - '"' << JSON.utf8_to_json(string) << '"' + '"' << JSON.utf8_to_json(string, state.escape_slash) << '"' end end diff --git a/tests/json_generator_test.rb b/tests/json_generator_test.rb old mode 100644 new mode 100755 index ee19fa5..13d3b5a --- a/tests/json_generator_test.rb +++ b/tests/json_generator_test.rb @@ -174,6 +174,7 @@ EOT :ascii_only => false, :buffer_initial_length => 1024, :depth => 0, + :escape_slash => false, :indent => " ", :max_nesting => 100, :object_nl => "\n", @@ -190,6 +191,7 @@ EOT :ascii_only => false, :buffer_initial_length => 1024, :depth => 0, + :escape_slash => false, :indent => "", :max_nesting => 100, :object_nl => "", @@ -206,6 +208,7 @@ EOT :ascii_only => false, :buffer_initial_length => 1024, :depth => 0, + :escape_slash => false, :indent => "", :max_nesting => 0, :object_nl => "", @@ -394,6 +397,10 @@ EOT json = '["/"]' assert_equal json, generate(data) # + data = [ '/' ] + json = '["\/"]' + assert_equal json, generate(data, :escape_slash => true) + # data = ['"'] json = '["\""]' assert_equal json, generate(data) diff --git a/tests/json_parser_test.rb b/tests/json_parser_test.rb index 9946dd9..514441e 100644 --- a/tests/json_parser_test.rb +++ b/tests/json_parser_test.rb @@ -293,6 +293,10 @@ EOT json = '["\\\'"]' data = ["'"] assert_equal data, parse(json) + + json = '["\/"]' + data = [ '/' ] + assert_equal data, parse(json) end class SubArray < Array -- cgit v1.2.1