From 5cab0ccd5fc68838939ea18018c1a4a1add25c4b Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Wed, 28 Mar 2018 15:17:08 -0500 Subject: [PATCH] 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 --- .../org/eclipse/jetty/util/ajax/JSON.java | 113 ++++++++++++++-- .../org/eclipse/jetty/util/ajax/JSONTest.java | 121 ++++++++++++++---- 2 files changed, 200 insertions(+), 34 deletions(-) diff --git a/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java b/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java index 24295d1ee69..d7367d6ca61 100644 --- a/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java +++ b/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java @@ -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. + * + * + * protected void escapeUnicode(Appendable buffer, char c) throws IOException + * { + * // Unicode is slash-u escaped + * buffer.append(String.format("\\u%04x", (int)c)); + * } + * + * + * @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 diff --git a/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/JSONTest.java b/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/JSONTest.java index 015a6c9354e..bc64e713d13 100644 --- a/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/JSONTest.java +++ b/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/JSONTest.java @@ -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 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 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 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 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 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 {