Issue #2391 - JSON string escaping fix + override

+ all string escaping now done in JSON.escapeString()
+ special override JSON.escapeUnicode() available for
  those that want to use optional Unicode escaping.
  javadoc indicates one way to accomplish this.

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2018-03-28 15:17:08 -05:00
parent 464b74ed60
commit 5cab0ccd5f
2 changed files with 200 additions and 34 deletions

View File

@ -32,7 +32,6 @@ import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
@ -99,6 +98,15 @@ public class JSON
{
}
/**
* Reset the default JSON behaviors to default
*/
public static void reset()
{
DEFAULT._convertors.clear();
DEFAULT._stringBufferSize = 1024;
}
/**
* @return the initial stringBuffer size to use when creating JSON strings
* (default 1024)
@ -236,6 +244,96 @@ public class JSON
return DEFAULT.parse(new StringSource(IO.toString(in)),stripOuterComment);
}
private void quotedEscape(Appendable buffer, String input)
{
try
{
buffer.append('"');
if (input != null && input.length() > 0)
{
escapeString(buffer, input);
}
buffer.append('"');
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
public void escapeString(Appendable buffer, String input) throws IOException
{
// default escaping here.
for (int i = 0; i < input.length(); ++i)
{
char c = input.charAt(i);
// ASCII printable range
if ((c >= 0x20) && (c <= 0x7E))
{
// Special cases for quotation-mark, reverse-solidus, and solidus
if ((c == '"') || (c == '\\') || (c == '/'))
{
buffer.append('\\').append(c);
}
else
{
// ASCII printable (that isn't escaped above)
buffer.append(c);
}
}
else
{
// All other characters are escaped (in some way)
// First we deal with the special short-form escaping
if (c == '\b') // backspace
buffer.append("\\b");
else if (c == '\f') // form-feed
buffer.append("\\f");
else if (c == '\n') // line feed
buffer.append("\\n");
else if (c == '\r') // carriage return
buffer.append("\\r");
else if (c == '\t') // tab
buffer.append("\\t");
else if (c < 0x20 || c == 0x7F) // all control characters
{
// default behavior is to encode
buffer.append(String.format("\\u%04x", (short)c));
}
else
{
// optional behavior in JSON spec
escapeUnicode(buffer, c);
}
}
}
}
/**
* Per spec, unicode characters are by default NOT escaped.
* This overridable allows for alternate behavior to escape those with your choice
* of encoding.
*
* <code>
* protected void escapeUnicode(Appendable buffer, char c) throws IOException
* {
* // Unicode is slash-u escaped
* buffer.append(String.format("\\u%04x", (int)c));
* }
* </code>
*
* @param buffer
* @param c
* @throws IOException
*/
protected void escapeUnicode(Appendable buffer, char c) throws IOException
{
buffer.append(c);
}
/**
* Convert Object to JSON
*
@ -428,7 +526,7 @@ public class JSON
while (iter.hasNext())
{
Map.Entry<?,?> entry = (Map.Entry<?,?>)iter.next();
QuotedStringTokenizer.quote(buffer,entry.getKey().toString());
quotedEscape(buffer, entry.getKey().toString());
buffer.append(':');
append(buffer,entry.getValue());
if (iter.hasNext())
@ -573,7 +671,7 @@ public class JSON
return;
}
QuotedStringTokenizer.quote(buffer,string);
quotedEscape(buffer,string);
}
// Parsing utilities
@ -1353,7 +1451,7 @@ public class JSON
if (c == 0)
throw new IllegalStateException();
_buffer.append(c);
QuotedStringTokenizer.quote(_buffer,name);
quotedEscape(_buffer,name);
_buffer.append(':');
append(_buffer,value);
c = ',';
@ -1372,7 +1470,7 @@ public class JSON
if (c == 0)
throw new IllegalStateException();
_buffer.append(c);
QuotedStringTokenizer.quote(_buffer,name);
quotedEscape(_buffer,name);
_buffer.append(':');
appendNumber(_buffer, value);
c = ',';
@ -1391,7 +1489,7 @@ public class JSON
if (c == 0)
throw new IllegalStateException();
_buffer.append(c);
QuotedStringTokenizer.quote(_buffer,name);
quotedEscape(_buffer,name);
_buffer.append(':');
appendNumber(_buffer, value);
c = ',';
@ -1410,7 +1508,7 @@ public class JSON
if (c == 0)
throw new IllegalStateException();
_buffer.append(c);
QuotedStringTokenizer.quote(_buffer,name);
quotedEscape(_buffer,name);
_buffer.append(':');
appendBoolean(_buffer,value?Boolean.TRUE:Boolean.FALSE);
c = ',';
@ -1568,7 +1666,6 @@ public class JSON
public void add(String name, boolean value);
}
/* ------------------------------------------------------------ */
/**
* JSON Convertible object. Object can implement this interface in a similar
* way to the {@link Externalizable} interface is used to allow classes to

View File

@ -18,9 +18,14 @@
package org.eclipse.jetty.util.ajax;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Array;
import java.math.BigDecimal;
@ -32,7 +37,7 @@ import java.util.TimeZone;
import org.eclipse.jetty.util.DateCache;
import org.eclipse.jetty.util.ajax.JSON.Output;
import org.junit.BeforeClass;
import org.junit.Before;
import org.junit.Test;
@ -55,15 +60,17 @@ public class JSONTest
"\"undefined\": undefined," +
"}";
@BeforeClass
public static void setUp() throws Exception
@Before
public void resetJSON()
{
JSON.registerConvertor(Gadget.class,new JSONObjectConvertor(false));
// Reset JSON configuration to default with each testcase
JSON.reset();
}
@Test
public void testToString()
{
JSON.registerConvertor(Gadget.class,new JSONObjectConvertor(false));
HashMap map = new HashMap();
HashMap obj6 = new HashMap();
HashMap obj7 = new HashMap();
@ -109,14 +116,13 @@ public class JSONTest
gadget.setWoggles(new Woggle[]{w0,w1});
s = JSON.toString(new Gadget[]{gadget});
assertTrue(s.startsWith("["));
assertTrue(s.indexOf("\"modulated\":false")>=0);
assertTrue(s.indexOf("\"shields\":42")>=0);
assertTrue(s.indexOf("\"name\":\"woggle0\"")>=0);
assertTrue(s.indexOf("\"name\":\"woggle1\"")>=0);
assertThat(s, startsWith("["));
assertThat(s, containsString("\"modulated\":false"));
assertThat(s, containsString("\"shields\":42"));
assertThat(s, containsString("\"name\":\"woggle0\""));
assertThat(s, containsString("\"name\":\"woggle1\""));
}
/* ------------------------------------------------------------ */
@Test
public void testParse()
{
@ -137,7 +143,84 @@ public class JSONTest
map = (Map)JSON.parse(test);
}
/* ------------------------------------------------------------ */
@Test
public void testToString_LineFeed()
{
Map<String,String> map = new HashMap<>();
map.put("str", "line\nfeed");
String jsonStr = JSON.toString(map);
assertThat(jsonStr, is("{\"str\":\"line\\nfeed\"}"));
}
@Test
public void testToString_Tab()
{
Map<String,String> map = new HashMap<>();
map.put("str", "tab\tchar");
String jsonStr = JSON.toString(map);
assertThat(jsonStr, is("{\"str\":\"tab\\tchar\"}"));
}
@Test
public void testToString_Bel()
{
Map<String,String> map = new HashMap<>();
map.put("str", "ascii\u0007bel");
String jsonStr = JSON.toString(map);
assertThat(jsonStr, is("{\"str\":\"ascii\\u0007bel\"}"));
}
@Test
public void testToString_Utf8()
{
Map<String,String> map = new HashMap<>();
map.put("str", "japanese: 桟橋");
String jsonStr = JSON.toString(map);
assertThat(jsonStr, is("{\"str\":\"japanese: 桟橋\"}"));
}
@Test
public void testToJson_Utf8_Encoded()
{
JSON jsonUnicode = new JSON()
{
@Override
protected void escapeUnicode(Appendable buffer, char c) throws IOException
{
buffer.append(String.format("\\u%04x", (int)c));
}
};
Map<String,String> map = new HashMap<>();
map.put("str", "japanese: 桟橋");
String jsonStr = jsonUnicode.toJSON(map);
assertThat(jsonStr, is("{\"str\":\"japanese: \\u685f\\u6a4b\"}"));
}
@Test
public void testParse_Utf8_JsonEncoded()
{
String jsonStr = "{\"str\": \"japanese: \\u685f\\u6a4b\"}";
Map map = (Map)JSON.parse(jsonStr);
assertThat(map.get("str"), is("japanese: 桟橋"));
}
@Test
public void testParse_Utf8_JavaEncoded()
{
String jsonStr = "{\"str\": \"japanese: \u685f\u6a4b\"}";
Map map = (Map)JSON.parse(jsonStr);
assertThat(map.get("str"), is("japanese: 桟橋"));
}
@Test
public void testParse_Utf8_Raw()
{
String jsonStr = "{\"str\": \"japanese: 桟橋\"}";
Map map = (Map)JSON.parse(jsonStr);
assertThat(map.get("str"), is("japanese: 桟橋"));
}
@Test
public void testParseReader() throws Exception
{
@ -153,7 +236,6 @@ public class JSONTest
map = (Map)JSON.parse(test);
}
/* ------------------------------------------------------------ */
@Test
public void testStripComment()
{
@ -176,7 +258,6 @@ public class JSONTest
}
/* ------------------------------------------------------------ */
@Test
public void testQuote()
{
@ -186,7 +267,6 @@ public class JSONTest
assertEquals("abc123|\"|\\|/|\b|\f|\n|\r|\t|\uaaaa|",result);
}
/* ------------------------------------------------------------ */
@Test
public void testBigDecimal()
{
@ -199,7 +279,6 @@ public class JSONTest
}
/* ------------------------------------------------------------ */
@Test
public void testZeroByte()
{
@ -207,13 +286,11 @@ public class JSONTest
JSON.toString(withzero);
}
/* ------------------------------------------------------------ */
public static class Gadget
{
private boolean modulated;
private long shields;
private Woggle[] woggles;
/* ------------------------------------------------------------ */
/**
* @return the modulated
*/
@ -221,7 +298,6 @@ public class JSONTest
{
return modulated;
}
/* ------------------------------------------------------------ */
/**
* @param modulated the modulated to set
*/
@ -229,7 +305,6 @@ public class JSONTest
{
this.modulated=modulated;
}
/* ------------------------------------------------------------ */
/**
* @return the shields
*/
@ -237,7 +312,6 @@ public class JSONTest
{
return shields;
}
/* ------------------------------------------------------------ */
/**
* @param shields the shields to set
*/
@ -245,7 +319,6 @@ public class JSONTest
{
this.shields=shields;
}
/* ------------------------------------------------------------ */
/**
* @return the woggles
*/
@ -253,7 +326,6 @@ public class JSONTest
{
return woggles;
}
/* ------------------------------------------------------------ */
/**
* @param woggles the woggles to set
*/
@ -263,7 +335,6 @@ public class JSONTest
}
}
/* ------------------------------------------------------------ */
@Test
public void testConvertor()
{
@ -292,7 +363,7 @@ public class JSONTest
json.append(buf,map);
String js=buf.toString();
assertTrue(js.indexOf("\"date\":\"01/01/1970 00:00:00 GMT\"")>=0);
assertTrue(js.indexOf("\"date\":\"01\\/01\\/1970 00:00:00 GMT\"")>=0);
assertTrue(js.indexOf("org.eclipse.jetty.util.ajax.JSONTest$Woggle")>=0);
assertTrue(js.indexOf("org.eclipse.jetty.util.ajax.JSONTest$Gizmo")<0);
assertTrue(js.indexOf("\"tested\":true")>=0);
@ -406,7 +477,6 @@ public class JSONTest
}
/* ------------------------------------------------------------ */
public static class Gizmo
{
String name;
@ -437,7 +507,6 @@ public class JSONTest
}
}
/* ------------------------------------------------------------ */
public static class Woggle extends Gizmo implements JSON.Convertible
{