From 26d181059989279a79c433cedcd893b4f52e42ee Mon Sep 17 00:00:00 2001 From: Francois Chagnon Date: Tue, 15 Sep 2015 21:17:34 +0000 Subject: [PATCH] add config options for escape_slash --- ext/json/ext/generator/generator.c | 60 +++++++++++++++++++++------ 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 | 11 +++-- lib/json/common.rb | 6 ++- lib/json/pure/generator.rb | 38 ++++++++++++----- tests/test_json.rb | 7 +++- tests/test_json_generate.rb | 3 ++ 9 files changed, 132 insertions(+), 32 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 98748325..cb9b87ca 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -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_quirks_mode, 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. @@ -124,7 +124,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); @@ -171,12 +171,14 @@ static void convert_UTF8_to_JSON_ASCII(FBuffer *buffer, VALUE string) case '\\': fbuffer_append(buffer, "\\\\", 2); break; - case '/': - fbuffer_append(buffer, "\\/", 2); - break; case '"': fbuffer_append(buffer, "\\\"", 2); break; + case '/': + if(escape_slash) { + fbuffer_append(buffer, "\\/", 2); + break; + } default: fbuffer_append_char(buffer, (char)ch); break; @@ -225,7 +227,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; @@ -271,14 +273,16 @@ static void convert_UTF8_to_JSON(FBuffer *buffer, VALUE string) escape = "\\\\"; escape_len = 2; break; - case '/': - escape = "\\/"; - escape_len = 2; - break; case '"': escape = "\\\""; escape_len = 2; break; + case '/': + if(escape_slash) { + escape = "\\/"; + escape_len = 2; + break; + } default: { unsigned short clen = trailingBytesForUTF8[c] + 1; @@ -631,6 +635,8 @@ static VALUE cState_configure(VALUE self, VALUE opts) state->ascii_only = RTEST(tmp); tmp = rb_hash_aref(opts, ID2SYM(i_quirks_mode)); state->quirks_mode = RTEST(tmp); + tmp = rb_hash_aref(opts, ID2SYM(i_escape_slash)); + state->escape_slash = RTEST(tmp); return self; } @@ -666,6 +672,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_quirks_mode), state->quirks_mode ? 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; @@ -799,9 +806,9 @@ static void generate_json_string(FBuffer *buffer, VALUE Vstate, JSON_Generator_S obj = rb_funcall(obj, i_encode, 1, CEncoding_UTF_8); #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, '"'); } @@ -1252,6 +1259,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? * @@ -1384,6 +1416,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); @@ -1439,6 +1474,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_quirks_mode = rb_intern("quirks_mode"); diff --git a/ext/json/ext/generator/generator.h b/ext/json/ext/generator/generator.h index 298c0a49..ea68e0e6 100644 --- a/ext/json/ext/generator/generator.h +++ b/ext/json/ext/generator/generator.h @@ -50,8 +50,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 */ @@ -74,6 +74,7 @@ typedef struct JSON_Generator_StateStruct { char allow_nan; char ascii_only; char quirks_mode; + char escape_slash; long depth; long buffer_initial_length; } JSON_Generator_State; @@ -145,6 +146,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 ecceb270..e6c8c28d 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -136,7 +136,7 @@ public RuntimeInfo getInfo() { 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 30653074..ecb73f56 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -83,6 +83,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.) @@ -172,6 +178,9 @@ static GeneratorState fromState(ThreadContext context, RuntimeInfo info, * -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) { @@ -195,6 +204,7 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) { 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; @@ -381,6 +391,24 @@ public IRubyObject max_nesting_set(IRubyObject max_nesting) { 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; } @@ -482,6 +510,7 @@ public IRubyObject configure(ThreadContext context, IRubyObject vOpts) { allowNaN = opts.getBool("allow_nan", DEFAULT_ALLOW_NAN); asciiOnly = opts.getBool("ascii_only", DEFAULT_ASCII_ONLY); quirksMode = opts.getBool("quirks_mode", DEFAULT_QUIRKS_MODE); + escapeSlash = opts.getBool("escape_slash", DEFAULT_ESCAPE_SLASH); bufferInitialLength = opts.getInt("buffer_initial_length", DEFAULT_BUFFER_INITIAL_LENGTH); depth = opts.getInt("depth", 0); @@ -510,6 +539,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("quirks_mode"), quirks_mode_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 fe1a8143..44e69d10 100644 --- a/java/src/json/ext/StringEncoder.java +++ b/java/src/json/ext/StringEncoder.java @@ -10,7 +10,7 @@ * 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 @@ -32,9 +32,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) { @@ -50,7 +51,6 @@ void encode(ByteList src, ByteList out) { private void handleChar(int c) { switch (c) { case '"': - case '/': case '\\': escapeChar((char)c); break; @@ -69,6 +69,11 @@ private void handleChar(int c) { 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 f44184e1..c16de2b4 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -358,12 +358,14 @@ class << self # :max_nesting: false # :allow_nan: true # :quirks_mode: true + # :escape_slash: true attr_accessor :dump_default_options end self.dump_default_options = { :max_nesting => false, :allow_nan => true, :quirks_mode => true, + :escape_slash => true, } # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns @@ -443,7 +445,7 @@ module ::Kernel # one line. def j(*objs) objs.each do |obj| - puts JSON::generate(obj, :allow_nan => true, :max_nesting => false) + puts JSON::generate(obj, :allow_nan => true, :max_nesting => false, :escape_slash => true) end nil end @@ -452,7 +454,7 @@ def j(*objs) # indentation and over many lines. def jj(*objs) objs.each do |obj| - puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false) + puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false, :escape_slash => true) end nil end diff --git a/lib/json/pure/generator.rb b/lib/json/pure/generator.rb index 36d9f421..e2676095 100644 --- a/lib/json/pure/generator.rb +++ b/lib/json/pure/generator.rb @@ -40,18 +40,20 @@ module JSON # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with # UTF16 big endian characters as \u????, and return it. if defined?(::Encoding) - def utf8_to_json(string) # :nodoc: + def utf8_to_json(string, escape_slash = true) # :nodoc: string = string.dup string.force_encoding(::Encoding::ASCII_8BIT) - string.gsub!(/[\/"\\\x0-\x1f]/) { MAP[$&] } + string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] } + string.gsub!('/') { MAP[$&] } if escape_slash string.force_encoding(::Encoding::UTF_8) string end - def utf8_to_json_ascii(string) # :nodoc: + def utf8_to_json_ascii(string, escape_slash = true) # :nodoc: string = string.dup string.force_encoding(::Encoding::ASCII_8BIT) - string.gsub!(/[\/"\\\x0-\x1f]/n) { MAP[$&] } + string.gsub!(/["\\\x0-\x1f]/n) { MAP[$&] } + string.gsub!('/') { MAP[$&] } if escape_slash string.gsub!(/( (?: [\xc2-\xdf][\x80-\xbf] | @@ -79,12 +81,15 @@ def valid_utf8?(string) end module_function :valid_utf8? else - def utf8_to_json(string) # :nodoc: - string.gsub(/[\/"\\\x0-\x1f]/n) { MAP[$&] } + def utf8_to_json(string, escape_slash = true) # :nodoc: + string = string.gsub(/["\\\x0-\x1f]/n) { MAP[$&] } + string.gsub!('/') { MAP[$&] } if escape_slash + string end - def utf8_to_json_ascii(string) # :nodoc: + def utf8_to_json_ascii(string, escape_slash = true) # :nodoc: string = string.gsub(/[\/"\\\x0-\x1f]/) { MAP[$&] } + string.gsub!('/') { MAP[$&] } if escape_slash string.gsub!(/( (?: [\xc2-\xdf][\x80-\xbf] | @@ -167,6 +172,7 @@ def initialize(opts = {}) @ascii_only = false @quirks_mode = false @buffer_initial_length = 1024 + @escape_slash = false configure opts end @@ -198,6 +204,10 @@ def initialize(opts = {}) # :stopdoc: attr_reader :buffer_initial_length + # If this attribute is set to true, forward slashes will be escaped in + # all json strings. + attr_accessor :escape_slash + def buffer_initial_length=(length) if length > 0 @buffer_initial_length = length @@ -239,6 +249,11 @@ def quirks_mode? @quirks_mode 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) @@ -262,6 +277,7 @@ def configure(opts) @depth = opts[:depth] || 0 @quirks_mode = opts[:quirks_mode] if opts.key?(:quirks_mode) @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 @@ -450,9 +466,9 @@ def to_json(state = nil, *args) 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 else @@ -462,9 +478,9 @@ def to_json(state = nil, *args) def to_json(state = nil, *args) state = State.from_state(state) if state.ascii_only? - '"' << JSON.utf8_to_json_ascii(self) << '"' + '"' << JSON.utf8_to_json_ascii(self, state.escape_slash) << '"' else - '"' << JSON.utf8_to_json(self) << '"' + '"' << JSON.utf8_to_json(self, state.escape_slash) << '"' end end end diff --git a/tests/test_json.rb b/tests/test_json.rb index aa4f75ce..8e619606 100755 --- a/tests/test_json.rb +++ b/tests/test_json.rb @@ -410,10 +410,15 @@ def test_backslash assert_equal data, JSON.parse(json) # data = [ '/' ] - json = '["\/"]' + json = '["/"]' assert_equal json, JSON.generate(data) assert_equal data, JSON.parse(json) # + data = [ '/' ] + json = '["\/"]' + assert_equal json, JSON.generate(data, :escape_slash => true) + assert_equal data, JSON.parse(json) + # json = '["\""]' data = JSON.parse(json) assert_equal ['"'], data diff --git a/tests/test_json_generate.rb b/tests/test_json_generate.rb index 8db0b789..1e0ec931 100755 --- a/tests/test_json_generate.rb +++ b/tests/test_json_generate.rb @@ -142,6 +142,7 @@ def test_pretty_state :buffer_initial_length => 1024, :quirks_mode => false, :depth => 0, + :escape_slash => false, :indent => " ", :max_nesting => 100, :object_nl => "\n", @@ -159,6 +160,7 @@ def test_safe_state :buffer_initial_length => 1024, :quirks_mode => false, :depth => 0, + :escape_slash => false, :indent => "", :max_nesting => 100, :object_nl => "", @@ -176,6 +178,7 @@ def test_fast_state :buffer_initial_length => 1024, :quirks_mode => false, :depth => 0, + :escape_slash => false, :indent => "", :max_nesting => 0, :object_nl => "",