Skip to content

Commit

Permalink
Add an option to escape forward slash character
Browse files Browse the repository at this point in the history
Squashed commit of the following:

commit 26d1810
Author: Francois Chagnon <[email protected]>
Date:   Tue Sep 15 21:17:34 2015 +0000

    add config options for escape_slash

commit fa28233
Author: Francois Chagnon <[email protected]>
Date:   Mon Feb 9 21:09:33 2015 +0000

    add forward slash to escape character
  • Loading branch information
byroot committed Jan 30, 2020
1 parent 3b9ecef commit d04ebdf
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 20 deletions.
53 changes: 48 additions & 5 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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, '"');
}
Expand Down Expand Up @@ -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?
*
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
7 changes: 5 additions & 2 deletions ext/json/ext/generator/generator.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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;
Expand Down Expand Up @@ -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)))
Expand Down
2 changes: 1 addition & 1 deletion java/src/json/ext/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
30 changes: 30 additions & 0 deletions java/src/json/ext/GeneratorState.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>true</code> 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.)
Expand Down Expand Up @@ -171,6 +177,9 @@ static GeneratorState fromState(ThreadContext context, RuntimeInfo info,
* <code>-Infinity</code> should be generated, otherwise an exception is
* thrown if these values are encountered.
* This options defaults to <code>false</code>.
* <dt><code>:escape_slash</code>
* <dd>set to <code>true</code> if the forward slashes should be escaped
* in the json output (default: <code>false</code>)
*/
@JRubyMethod(optional=1, visibility=Visibility.PRIVATE)
public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()) {
Expand Down
10 changes: 8 additions & 2 deletions java/src/json/ext/StringEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions json_pure.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ 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 = "[email protected]".freeze
s.extra_rdoc_files = ["README.md".freeze]
s.files = ["./tests/test_helper.rb".freeze, ".gitignore".freeze, ".travis.yml".freeze, "CHANGES.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README-json-jruby.md".freeze, "README.md".freeze, "Rakefile".freeze, "VERSION".freeze, "diagrams/.keep".freeze, "ext/json/ext/fbuffer/fbuffer.h".freeze, "ext/json/ext/generator/depend".freeze, "ext/json/ext/generator/extconf.rb".freeze, "ext/json/ext/generator/generator.c".freeze, "ext/json/ext/generator/generator.h".freeze, "ext/json/ext/parser/depend".freeze, "ext/json/ext/parser/extconf.rb".freeze, "ext/json/ext/parser/parser.c".freeze, "ext/json/ext/parser/parser.h".freeze, "ext/json/ext/parser/parser.rl".freeze, "ext/json/extconf.rb".freeze, "install.rb".freeze, "java/src/json/ext/ByteListTranscoder.java".freeze, "java/src/json/ext/Generator.java".freeze, "java/src/json/ext/GeneratorMethods.java".freeze, "java/src/json/ext/GeneratorService.java".freeze, "java/src/json/ext/GeneratorState.java".freeze, "java/src/json/ext/OptionsReader.java".freeze, "java/src/json/ext/Parser.java".freeze, "java/src/json/ext/Parser.rl".freeze, "java/src/json/ext/ParserService.java".freeze, "java/src/json/ext/RuntimeInfo.java".freeze, "java/src/json/ext/StringDecoder.java".freeze, "java/src/json/ext/StringEncoder.java".freeze, "java/src/json/ext/Utils.java".freeze, "json-java.gemspec".freeze, "json.gemspec".freeze, "json_pure.gemspec".freeze, "lib/json.rb".freeze, "lib/json/add/bigdecimal.rb".freeze, "lib/json/add/complex.rb".freeze, "lib/json/add/core.rb".freeze, "lib/json/add/date.rb".freeze, "lib/json/add/date_time.rb".freeze, "lib/json/add/exception.rb".freeze, "lib/json/add/ostruct.rb".freeze, "lib/json/add/range.rb".freeze, "lib/json/add/rational.rb".freeze, "lib/json/add/regexp.rb".freeze, "lib/json/add/set.rb".freeze, "lib/json/add/struct.rb".freeze, "lib/json/add/symbol.rb".freeze, "lib/json/add/time.rb".freeze, "lib/json/common.rb".freeze, "lib/json/ext.rb".freeze, "lib/json/ext/.keep".freeze, "lib/json/generic_object.rb".freeze, "lib/json/pure.rb".freeze, "lib/json/pure/generator.rb".freeze, "lib/json/pure/parser.rb".freeze, "lib/json/version.rb".freeze, "references/rfc7159.txt".freeze, "tests/fixtures/fail10.json".freeze, "tests/fixtures/fail11.json".freeze, "tests/fixtures/fail12.json".freeze, "tests/fixtures/fail13.json".freeze, "tests/fixtures/fail14.json".freeze, "tests/fixtures/fail18.json".freeze, "tests/fixtures/fail19.json".freeze, "tests/fixtures/fail2.json".freeze, "tests/fixtures/fail20.json".freeze, "tests/fixtures/fail21.json".freeze, "tests/fixtures/fail22.json".freeze, "tests/fixtures/fail23.json".freeze, "tests/fixtures/fail24.json".freeze, "tests/fixtures/fail25.json".freeze, "tests/fixtures/fail27.json".freeze, "tests/fixtures/fail28.json".freeze, "tests/fixtures/fail3.json".freeze, "tests/fixtures/fail4.json".freeze, "tests/fixtures/fail5.json".freeze, "tests/fixtures/fail6.json".freeze, "tests/fixtures/fail7.json".freeze, "tests/fixtures/fail8.json".freeze, "tests/fixtures/fail9.json".freeze, "tests/fixtures/obsolete_fail1.json".freeze, "tests/fixtures/pass1.json".freeze, "tests/fixtures/pass15.json".freeze, "tests/fixtures/pass16.json".freeze, "tests/fixtures/pass17.json".freeze, "tests/fixtures/pass2.json".freeze, "tests/fixtures/pass26.json".freeze, "tests/fixtures/pass3.json".freeze, "tests/json_addition_test.rb".freeze, "tests/json_common_interface_test.rb".freeze, "tests/json_encoding_test.rb".freeze, "tests/json_ext_parser_test.rb".freeze, "tests/json_fixtures_test.rb".freeze, "tests/json_generator_test.rb".freeze, "tests/json_generic_object_test.rb".freeze, "tests/json_parser_test.rb".freeze, "tests/json_string_matching_test.rb".freeze, "tests/test_helper.rb".freeze, "tools/diff.sh".freeze, "tools/fuzz.rb".freeze, "tools/server.rb".freeze]
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]
Expand Down
6 changes: 4 additions & 2 deletions lib/json/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
28 changes: 22 additions & 6 deletions lib/json/pure/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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] |
Expand Down Expand Up @@ -148,6 +154,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

Expand Down Expand Up @@ -187,6 +197,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)
Expand All @@ -209,6 +224,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
Expand Down Expand Up @@ -399,9 +415,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

Expand Down
Loading

0 comments on commit d04ebdf

Please sign in to comment.