Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an option to escape forward slash character #405

Merged
merged 1 commit into from Jul 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 48 additions & 5 deletions ext/json/ext/generator/generator.c
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
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
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
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
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
3 changes: 2 additions & 1 deletion lib/json/common.rb
Expand Up @@ -436,12 +436,13 @@ class << self
# 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
Expand Down
30 changes: 24 additions & 6 deletions lib/json/pure/generator.rb
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 = 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] |
Expand Down Expand Up @@ -109,6 +115,7 @@ def self.from_state(opts)
# * *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.
Expand All @@ -123,6 +130,7 @@ def initialize(opts = {})
@array_nl = ''
casperisfine marked this conversation as resolved.
Show resolved Hide resolved
@allow_nan = false
@ascii_only = false
@escape_slash = false
@buffer_initial_length = 1024
configure opts
end
Expand All @@ -148,6 +156,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 +199,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 +226,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?(:escape_slash)

if !opts.key?(:max_nesting) # defaults to 100
@max_nesting = 100
Expand Down Expand Up @@ -399,9 +417,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