diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index 67e1af27..bfb63400 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -22,6 +22,8 @@ import com.google.gson.Strictness; import com.google.gson.internal.DefaultConfig; import com.google.gson.internal.JsonReaderInternalAccess; import com.google.gson.internal.TroubleshootingGuide; +import com.google.gson.util.StringEscapeUtil; + import java.io.Closeable; import java.io.EOFException; import java.io.IOException; @@ -1611,7 +1613,11 @@ public class JsonReader implements Closeable { private String locationString() { int line = lineNumber + 1; int column = pos - lineStart + 1; - String charInterjection = pos < buffer.length ? " (char '" + buffer[pos] + "')" : ""; + String replacement = StringEscapeUtil.getReplacement(buffer[pos]); + if (replacement == null) { + replacement = String.valueOf(buffer[pos]); + } + String charInterjection = pos < buffer.length ? " (char '" + replacement + "')" : ""; return " at line " + line + " column " + column + charInterjection + " path " + getPath(); } diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java index 1d32a466..4f6e00ba 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -30,6 +30,8 @@ import com.google.gson.FormattingStyle; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.Strictness; +import com.google.gson.util.StringEscapeUtil; + import java.io.Closeable; import java.io.Flushable; import java.io.IOException; @@ -166,39 +168,6 @@ public class JsonWriter implements Closeable, Flushable { private static final Pattern VALID_JSON_NUMBER_PATTERN = Pattern.compile("-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][-+]?[0-9]+)?"); - /* - * From RFC 8259, "All Unicode characters may be placed within the - * quotation marks except for the characters that must be escaped: - * quotation mark, reverse solidus, and the control characters - * (U+0000 through U+001F)." - * - * We also escape '\u2028' and '\u2029', which JavaScript interprets as - * newline characters. This prevents eval() from failing with a syntax - * error. http://code.google.com/p/google-gson/issues/detail?id=341 - */ - private static final String[] REPLACEMENT_CHARS; - private static final String[] HTML_SAFE_REPLACEMENT_CHARS; - - static { - REPLACEMENT_CHARS = new String[128]; - for (int i = 0; i <= 0x1f; i++) { - REPLACEMENT_CHARS[i] = String.format("\\u%04x", i); - } - REPLACEMENT_CHARS['"'] = "\\\""; - REPLACEMENT_CHARS['\\'] = "\\\\"; - REPLACEMENT_CHARS['\t'] = "\\t"; - REPLACEMENT_CHARS['\b'] = "\\b"; - REPLACEMENT_CHARS['\n'] = "\\n"; - REPLACEMENT_CHARS['\r'] = "\\r"; - REPLACEMENT_CHARS['\f'] = "\\f"; - HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone(); - HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c"; - HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e"; - HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026"; - HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d"; - HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027"; - } - /** The JSON output destination */ private final Writer out; @@ -794,23 +763,13 @@ public class JsonWriter implements Closeable, Flushable { } private void string(String value) throws IOException { - String[] replacements = htmlSafe ? HTML_SAFE_REPLACEMENT_CHARS : REPLACEMENT_CHARS; out.write('\"'); int last = 0; int length = value.length(); for (int i = 0; i < length; i++) { char c = value.charAt(i); - String replacement; - if (c < 128) { - replacement = replacements[c]; - if (replacement == null) { - continue; - } - } else if (c == '\u2028') { - replacement = "\\u2028"; - } else if (c == '\u2029') { - replacement = "\\u2029"; - } else { + String replacement = htmlSafe ? StringEscapeUtil.getHtmlSafeReplacement(c) : StringEscapeUtil.getReplacement(c); + if (replacement == null) { continue; } if (last < i) { diff --git a/gson/src/main/java/com/google/gson/util/StringEscapeUtil.java b/gson/src/main/java/com/google/gson/util/StringEscapeUtil.java new file mode 100644 index 00000000..6e06e622 --- /dev/null +++ b/gson/src/main/java/com/google/gson/util/StringEscapeUtil.java @@ -0,0 +1,80 @@ +package com.google.gson.util; + +/** + * Utilities methods for escaping strings, extracted from gsons JsonWriter. + * @author JFronny + */ +public class StringEscapeUtil { + private StringEscapeUtil() {} + + /* + * From RFC 8259, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + * + * We also escape '\u2028' and '\u2029', which JavaScript interprets as + * newline characters. This prevents eval() from failing with a syntax + * error. http://code.google.com/p/google-gson/issues/detail?id=341 + */ + private static final String[] REPLACEMENT_CHARS; + private static final String[] HTML_SAFE_REPLACEMENT_CHARS; + + static { + REPLACEMENT_CHARS = new String[128]; + for (int i = 0; i <= 0x1f; i++) { + REPLACEMENT_CHARS[i] = String.format("\\u%04x", i); + } + REPLACEMENT_CHARS['"'] = "\\\""; + REPLACEMENT_CHARS['\\'] = "\\\\"; + REPLACEMENT_CHARS['\t'] = "\\t"; + REPLACEMENT_CHARS['\b'] = "\\b"; + REPLACEMENT_CHARS['\n'] = "\\n"; + REPLACEMENT_CHARS['\r'] = "\\r"; + REPLACEMENT_CHARS['\f'] = "\\f"; + REPLACEMENT_CHARS['\0'] = "\\0"; + HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone(); + HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c"; + HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e"; + HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026"; + HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d"; + HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027"; + } + + private static String getReplacement(char c, String[] replacements) { + String replacement; + if (c < 128) { + replacement = replacements[c]; + if (replacement == null) { + return null; + } + } else if (c == '\u2028') { + replacement = "\\u2028"; + } else if (c == '\u2029') { + replacement = "\\u2029"; + } else { + return null; + } + return replacement; + } + + /** + * Returns the replacement for the character, or null if the character does not need to be escaped. + * @param c the character to escape + * @return the replacement for the character, or null if the character does not need to be escaped + * @see #getHtmlSafeReplacement(char) + */ + public static String getReplacement(char c) { + return getReplacement(c, REPLACEMENT_CHARS); + } + + /** + * Returns the replacement for the character, or null if the character does not need to be escaped. + * @param c the character to escape + * @return the replacement for the character, or null if the character does not need to be escaped + * @see #getReplacement(char) + */ + public static String getHtmlSafeReplacement(char c) { + return getReplacement(c, HTML_SAFE_REPLACEMENT_CHARS); + } +} diff --git a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java index 3dc7ba0c..4c20a5ae 100644 --- a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java +++ b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java @@ -40,7 +40,7 @@ public class ToNumberPolicyTest { assertThat(e) .hasMessageThat() .isEqualTo( - "JSON forbids NaN and infinities: Infinity at line 1 column 6 (char '\0') path $\n" + "JSON forbids NaN and infinities: Infinity at line 1 column 6 (char '\\0') path $\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); assertThrows( @@ -133,7 +133,7 @@ public class ToNumberPolicyTest { assertThat(e) .hasMessageThat() .isEqualTo( - "Expected a double but was NULL at line 1 column 5 (char '\0') path $\n" + "Expected a double but was NULL at line 1 column 5 (char '\\0') path $\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); e = @@ -143,7 +143,7 @@ public class ToNumberPolicyTest { assertThat(e) .hasMessageThat() .isEqualTo( - "Expected a string but was NULL at line 1 column 5 (char '\0') path $\n" + "Expected a string but was NULL at line 1 column 5 (char '\\0') path $\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); e = @@ -153,7 +153,7 @@ public class ToNumberPolicyTest { assertThat(e) .hasMessageThat() .isEqualTo( - "Expected a string but was NULL at line 1 column 5 (char '\0') path $\n" + "Expected a string but was NULL at line 1 column 5 (char '\\0') path $\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); e = @@ -163,7 +163,7 @@ public class ToNumberPolicyTest { assertThat(e) .hasMessageThat() .isEqualTo( - "Expected a string but was NULL at line 1 column 5 (char '\0') path $\n" + "Expected a string but was NULL at line 1 column 5 (char '\\0') path $\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#adapter-not-null-safe"); } diff --git a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java index bb01cf3b..9b735cb6 100644 --- a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java +++ b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java @@ -97,7 +97,7 @@ public class PrimitiveTest { .hasMessageThat() .isEqualTo( "java.lang.NumberFormatException: Expected an int but was 2147483648" - + " at line 1 column 11 (char '\0') path $"); + + " at line 1 column 11 (char '\\0') path $"); } @Test @@ -140,7 +140,7 @@ public class PrimitiveTest { .hasMessageThat() .isEqualTo( "java.lang.NumberFormatException: Expected an int but was 2147483648" - + " at line 1 column 11 (char '\0') path $"); + + " at line 1 column 11 (char '\\0') path $"); } @Test @@ -915,7 +915,7 @@ public class PrimitiveTest { assertThat(e) .hasCauseThat() .hasMessageThat() - .isEqualTo("Expected an int but was -122.08e-213 at line 1 column 13 (char '\0') path $"); + .isEqualTo("Expected an int but was -122.08e-213 at line 1 column 13 (char '\\0') path $"); } @Test @@ -926,7 +926,7 @@ public class PrimitiveTest { assertThat(e) .hasCauseThat() .hasMessageThat() - .isEqualTo("Expected an int but was " + number + " at line 1 column 57 (char '\0') path $"); + .isEqualTo("Expected an int but was " + number + " at line 1 column 57 (char '\\0') path $"); } @Test @@ -937,7 +937,7 @@ public class PrimitiveTest { assertThat(e) .hasCauseThat() .hasMessageThat() - .isEqualTo("Expected a long but was " + number + " at line 1 column 57 (char '\0') path $"); + .isEqualTo("Expected a long but was " + number + " at line 1 column 57 (char '\\0') path $"); } @Test diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index f007268b..8d1d3a8c 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -1175,27 +1175,27 @@ public final class JsonReaderTest { fail(); } catch (IllegalStateException expected) { assertUnexpectedStructureError( - expected, "a string", "END_OBJECT", "line 1 column 11 (char '\0') path $.a"); + expected, "a string", "END_OBJECT", "line 1 column 11 (char '\\0') path $.a"); } try { reader.nextName(); fail(); } catch (IllegalStateException expected) { - assertUnexpectedStructureError(expected, "a name", "END_OBJECT", "line 1 column 11 (char '\0') path $.a"); + assertUnexpectedStructureError(expected, "a name", "END_OBJECT", "line 1 column 11 (char '\\0') path $.a"); } try { reader.beginArray(); fail(); } catch (IllegalStateException expected) { assertUnexpectedStructureError( - expected, "BEGIN_ARRAY", "END_OBJECT", "line 1 column 11 (char '\0') path $.a"); + expected, "BEGIN_ARRAY", "END_OBJECT", "line 1 column 11 (char '\\0') path $.a"); } try { reader.endArray(); fail(); } catch (IllegalStateException expected) { assertUnexpectedStructureError( - expected, "END_ARRAY", "END_OBJECT", "line 1 column 11 (char '\0') path $.a"); + expected, "END_ARRAY", "END_OBJECT", "line 1 column 11 (char '\\0') path $.a"); } reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); @@ -1623,7 +1623,7 @@ public final class JsonReaderTest { reader.nextNull(); fail(); } catch (IllegalStateException expected) { - assertThat(expected).hasMessageThat().startsWith("Expected null but was END_ARRAY at line 1 column 8 (char '\0') path $[1]"); + assertThat(expected).hasMessageThat().startsWith("Expected null but was END_ARRAY at line 1 column 8 (char '\\0') path $[1]"); } reader = new JsonReader(reader("[,]")); @@ -2109,7 +2109,7 @@ public final class JsonReaderTest { assertThat(expected) .hasMessageThat() .isEqualTo( - "Unterminated object at x at line 1 column 16 (char '\0') path $.a\n" + "Unterminated object at x at line 1 column 16 (char '\\0') path $.a\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); } } @@ -2241,7 +2241,7 @@ public final class JsonReaderTest { assertThat(expected) .hasMessageThat() .isEqualTo( - "Expected name at line 1 column 11 (char '\0') path $.a\n" + "Expected name at line 1 column 11 (char '\\0') path $.a\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); } } @@ -2260,7 +2260,7 @@ public final class JsonReaderTest { assertThat(expected) .hasMessageThat() .isEqualTo( - "Expected name at line 1 column 11 (char '\0') path $.a\n" + "Expected name at line 1 column 11 (char '\\0') path $.a\n" + "See https://github.com/google/gson/blob/main/Troubleshooting.md#malformed-json"); } } diff --git a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java index 13857696..ca777c4e 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -698,7 +698,7 @@ public final class JsonWriterTest { + "\"}\"," + "\"[\"," + "\"]\"," - + "\"\\u0000\"," + + "\"\\0\"," + "\"\\u0019\"]"); }