diff --git a/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java b/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java index 195b3275bde..b5954611726 100644 --- a/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java +++ b/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java @@ -18,19 +18,16 @@ */ package org.apache.cxf.jaxrs.json.basic; +import org.apache.cxf.common.util.StringUtils; +import org.apache.cxf.helpers.IOUtils; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; import java.util.*; -import org.apache.cxf.common.util.StringUtils; -import org.apache.cxf.helpers.IOUtils; - - - public class JsonMapObjectReaderWriter { - private static final Set ESCAPED_CHARS; private static final char DQUOTE = '"'; private static final char COMMA = ','; private static final char COLON = ':'; @@ -41,18 +38,34 @@ public class JsonMapObjectReaderWriter { private static final char ESCAPE = '\\'; private static final String NULL_VALUE = "null"; private boolean format; + private final static int[] sOutputEscapes128; + private final static char[] HC = "0123456789ABCDEF".toCharArray(); + /** + * Value used for lookup tables to indicate that matching characters + * are to be escaped using standard escaping; for JSON this means + * (for example) using "backslash - u" escape method. + */ + + public final static int ESCAPE_STANDARD = -1; static { - Set chars = new HashSet<>(); - chars.add('"'); - chars.add('\\'); - chars.add('/'); - chars.add('b'); - chars.add('f'); - chars.add('h'); - chars.add('r'); - chars.add('t'); - ESCAPED_CHARS = Collections.unmodifiableSet(chars); + int[] table = new int[128]; + // Control chars need generic escape sequence + for (int i = 0; i < 32; ++i) { + table[i] = ESCAPE_STANDARD; + } + /* Others (and some within that range too) have explicit shorter + * sequences + */ + table['"'] = '"'; + table['\\'] = '\\'; + // Escaping of slash is optional, so let's not add it + table[0x08] = 'b'; + table[0x09] = 't'; + table[0x0C] = 'f'; + table[0x0A] = 'n'; + table[0x0D] = 'r'; + sOutputEscapes128 = table; } public JsonMapObjectReaderWriter() { @@ -384,26 +397,38 @@ public Output append(char ch) { } - private String escapeJson(String value) { + private String escapeJson(String content) { StringBuilder sb = new StringBuilder(); - for (int i = 0; i < value.length(); i++) { - char c = value.charAt(i); - // If we have " and the previous char was not \ then escape it - if (c == '"' && (i == 0 || value.charAt(i - 1) != '\\')) { - sb.append('\\').append(c); - // If we have \ and the previous char was not \ and the next char is not an escaped char, then escape it - } else if (c == '\\' && (i == 0 || value.charAt(i - 1) != '\\') - && (i == value.length() - 1 || !isEscapedChar(value.charAt(i + 1)))) { - sb.append('\\').append(c); - } else { + final int[] escCodes = sOutputEscapes128; + int escLen = escCodes.length; + for (int i = 0, len = content.length(); i < len; ++i) { + char c = content.charAt(i); + if (c >= escLen || escCodes[c] == 0) { sb.append(c); + continue; + } + sb.append('\\'); + int escCode = escCodes[c]; + if (escCode < 0) { // generic quoting (hex value) + // The only negative value sOutputEscapes128 returns + // is ESCAPE_STANDARD, which mean + // appendQuotes should encode using the Unicode encoding; + // not sure if this is the right way to encode for + // ESCAPE_CUSTOM or other (future) + // ESCAPE_XXX values. + + // We know that it has to fit in just 2 hex chars + sb.append('u'); + sb.append('0'); + sb.append('0'); + int value = c; // widening + sb.append(HC[value >> 4]); + sb.append(HC[value & 0xF]); + } else { // "named", i.e. prepend with slash + sb.append((char) escCode); } } return sb.toString(); } - private boolean isEscapedChar(char c) { - return ESCAPED_CHARS.contains(Character.valueOf(c)); - } - } diff --git a/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java b/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java index 424eb72c5dd..091353e600d 100644 --- a/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java +++ b/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java @@ -210,4 +210,19 @@ public void testAlreadyEscapedBackslash() throws Exception { assertEquals("a\\", entry.getValue()); } + @Test + public void testEscapingMetacharacter() throws Exception { + JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter(); + Map content = new HashMap<>(); + content.put("userInput", "GET\n\n\n\t\f\b\r \"\\"); + String json = jsonMapObjectReaderWriter.toJson(content); + assertEquals("{\"userInput\":\"GET\\n\\n\\n\\t\\f\\b\\r \\\"\\\\\"}", json); + + Map map = jsonMapObjectReaderWriter.fromJson(json); + assertEquals(1, map.size()); + Map.Entry entry = map.entrySet().iterator().next(); + assertEquals("userInput", entry.getKey()); + assertEquals("GET\\n\\n\\n\\t\\f\\b\\r \"\\", entry.getValue()); + } + }