From 318c353b59f9dd8e6a791ee99a60e5a3baa3a34e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 30 Jan 2020 12:48:05 +0100 Subject: [PATCH] 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 ++++- json_pure.gemspec | 4 +- lib/json/common.rb | 6 ++- lib/json/pure/generator.rb | 29 ++++++++++++--- tests/json_generator_test.rb | 8 ++++ 9 files changed, 129 insertions(+), 20 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 ae60c4f4..d5e43d3b 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 c367a620..3ebd6225 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 24bf0497..cd23c73b 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -158,7 +158,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 44a93113..7fec09d4 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 @@ 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) { @@ -194,6 +203,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; @@ -346,6 +356,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; } @@ -430,6 +458,7 @@ public IRubyObject configure(ThreadContext context, IRubyObject vOpts) { 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 RubyHash to_h(ThreadContext context) { 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 9d40dd38..26678ede 100644 --- a/java/src/json/ext/StringEncoder.java +++ b/java/src/json/ext/StringEncoder.java @@ -15,7 +15,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 @@ -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 @@ 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/json_pure.gemspec b/json_pure.gemspec index 08413d67..646015ad 100644 --- a/json_pure.gemspec +++ b/json_pure.gemspec @@ -8,7 +8,7 @@ Gem::Specification.new do |s| s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Florian Frank".freeze] - s.date = "2019-12-11" + s.date = "2020-01-30" s.description = "This is a JSON implementation in pure Ruby.".freeze s.email = "flori@ping.de".freeze s.extra_rdoc_files = ["README.md".freeze] @@ -16,7 +16,7 @@ Gem::Specification.new do |s| s.homepage = "http://flori.github.com/json".freeze s.licenses = ["Ruby".freeze] s.rdoc_options = ["--title".freeze, "JSON implemention for ruby".freeze, "--main".freeze, "README.md".freeze] - s.required_ruby_version = Gem::Requirement.new(">= 1.9".freeze) + s.required_ruby_version = Gem::Requirement.new(">= 2.0".freeze) s.rubygems_version = "3.0.3".freeze s.summary = "JSON Implementation for Ruby".freeze s.test_files = ["./tests/test_helper.rb".freeze] diff --git a/lib/json/common.rb b/lib/json/common.rb index 5ba8f1d2..ba292a91 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -362,11 +362,13 @@ class << self # :max_nesting: false # :allow_nan: true # :allow_blank: true + # :escape_slash: true attr_accessor :dump_default_options end self.dump_default_options = { :max_nesting => false, :allow_nan => true, + :escape_slash => true, } # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns @@ -418,7 +420,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 @@ -427,7 +429,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 a1143f8b..ae7f50a5 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 = true) # :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 = true) # :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] | @@ -123,6 +129,7 @@ def initialize(opts = {}) @array_nl = '' @allow_nan = false @ascii_only = false + @escape_slash = false @buffer_initial_length = 1024 configure opts end @@ -148,6 +155,10 @@ def initialize(opts = {}) # 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 +198,11 @@ def ascii_only? @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 +225,7 @@ def configure(opts) @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?(:max_nesting) # defaults to 100 @max_nesting = 100 @@ -399,9 +416,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 diff --git a/tests/json_generator_test.rb b/tests/json_generator_test.rb old mode 100644 new mode 100755 index ee19fa5e..fb147db3 --- a/tests/json_generator_test.rb +++ b/tests/json_generator_test.rb @@ -174,6 +174,7 @@ def test_pretty_state :ascii_only => false, :buffer_initial_length => 1024, :depth => 0, + :escape_slash => false, :indent => " ", :max_nesting => 100, :object_nl => "\n", @@ -190,6 +191,7 @@ def test_safe_state :ascii_only => false, :buffer_initial_length => 1024, :depth => 0, + :escape_slash => false, :indent => "", :max_nesting => 100, :object_nl => "", @@ -206,6 +208,7 @@ def test_fast_state :ascii_only => false, :buffer_initial_length => 1024, :depth => 0, + :escape_slash => false, :indent => "", :max_nesting => 0, :object_nl => "", @@ -401,6 +404,11 @@ def test_backslash data = ["'"] json = '["\\\'"]' assert_equal '["\'"]', generate(data) + + data = [ '/' ] + json = '["\/"]' + assert_equal json, JSON.generate(data, :escape_slash => true) + assert_equal data, JSON.parse(json) end def test_string_subclass