From 9ce668e10bb2aed284cb8b5ad0345f0dbea8670b Mon Sep 17 00:00:00 2001 From: Marten Date: Wed, 31 May 2023 01:54:44 +0200 Subject: [PATCH] Strict mode for JSON parsing (#2323) * Feat #6: Add strict flag to Gson and GsonBuilder * Test #2: Add failing tests for capitalized keywords * Feat #2: JsonReader does not read (partially) capitalized keywords if strict mode is used * Feat #3: Added implementation and tests for JSONReader not accepting specific escape sequence representing in strict mode * Test #3: Simplify test cases by removing unnecessary array * Feat #3: Improve error by including the illegal character * Feat #5: JsonReader does not allow unespaced control flow characters in strict mode * Test #5: Test unespaced control flow characters in strict mode * Feat #4: Disallow espaced newline character in strict mode * Test #4: Add tests for (dis)allowing newline character depensding on strictness * Test #5: Test case for unescaped control char in non-strict mode * Test #2: Simplify test cases * Feat #13: Change leniency API to Strictness enum in JsonReader, Gson, and GsonBuilder * Feat #15: Change JsonWriter API to also use Strictness * Test #15: Test Strictness in JsonWriter API * Doc #15: Add and update documentation for Strictness in JsonWriter API * refactor #12: Fixed typos and empty catch brackets in tests * refactor #12: Resolved importing wildcards, made some lines adhere to Google java style * #5 Add test case for unescaped control characters * Feat #5: add new lines to make JsonReader able to detect unescaped control characters (U+0000 through U+001F) and throw exceptions. * Feat #5: add new lines to make JsonReader able to detect unescaped control characters (U+0000 through U+001F) and throw exceptions. * Test #11: Added two tests for testing implementation of control character handling in strict mode and moved the implementation to nextQuotedValue * Test #11: Added two tests for testing implementation of control character handling in strict mode and moved the implementation to nextQuotedValue --------- Co-authored-by: LMC117 <2295699210@qq.com> Co-authored-by: Marten Voorberg * Doc #17: Add and change javadoc of public methods * Doc #17: Update JavaDoc in JsonReader and Strictness * Doc #17: Update JavaDoc in Gson and GsonBuilder * Test #34: Add tests for setting strictness through GsonBuilder * Fix: Add Fix broken test * Fix: Invalid JavaDoc in Gson.java * Doc #17: update outdated javadoc * #37: Resolve more PR feedback * Fix #37: Resolve various PR comments * Fix #37: Resolve various PR comments * Refactor #35: Refactor JsonReader#peekKeyword to reduce the amount of strictness checks (#39) * Doc #40: Update JavaDoc based on PR feedback * Doc #40: Update old RFC in GsonBuilder documentation * Doc #40: Fix formatting error in JavaDoc * Doc #40: Add tests for setting strictness and lenient to JsonReaderTest * Test #43: Changed tests to make use of assertThrows * test #43: Changed tests to make use of assertThrows as per feedback * Test #43: Update JsonWriterTest#testStrictnessNull to use assertThrows * Test #43: Update JsonWriterTest#testStrictnessNull to use assertThrows * test #43: Resolve PR recommendations * Test #43: Mini change to TC * Test #43: Mini change to TC --------- Co-authored-by: Marten Voorberg * doc #46: Resolved comments in main PR * Feat #45: Change Gson.fromJson and Gson.toJson to be strict when the provided writer/reader is strict * Fix #45: Small type * Update gson/src/test/java/com/google/gson/stream/JsonReaderTest.java Co-authored-by: Marcono1234 * Fix #45: Resolve various comments by Marcono1234 * Update gson/src/main/java/com/google/gson/GsonBuilder.java Co-authored-by: Marcono1234 * Fix #45: Resolve various comments by Marcono1234 * Fix #45: Resolve various comments by eamonmcmanus * Strictness mode follow-up * Update Troubleshooting.md and Gson default lenient mode documentation * Always use GSON strictness when set. * Rename Strictness.DEFAULT to Strictness.LEGACY_STRICT * Update JavaDoc with new strictness functionality * Replace default with legacy strict for JsonReader javadoc * Add JSONReader test cases for U2028 and U2029 * Refactor JSONReader#peekKeyWord() based on @eamonmcmanus's suggestion * Deprecate setLenient in favor of setStrictness --------- Co-authored-by: Carl Peterson Co-authored-by: Gustaf Johansson Co-authored-by: gustajoh <58432871+gustajoh@users.noreply.github.com> Co-authored-by: LMC117 <2295699210@qq.com> Co-authored-by: Marcono1234 --- Troubleshooting.md | 2 +- gson/src/main/java/com/google/gson/Gson.java | 110 ++++++++---- .../java/com/google/gson/GsonBuilder.java | 46 +++-- .../main/java/com/google/gson/Strictness.java | 29 ++++ .../com/google/gson/stream/JsonReader.java | 157 ++++++++++++----- .../com/google/gson/stream/JsonWriter.java | 82 ++++++--- .../java/com/google/gson/GsonBuilderTest.java | 29 ++++ .../test/java/com/google/gson/GsonTest.java | 9 +- .../internal/bind/JsonTreeReaderTest.java | 2 +- .../internal/bind/JsonTreeWriterTest.java | 1 + .../google/gson/stream/JsonReaderTest.java | 163 ++++++++++++++++++ .../google/gson/stream/JsonWriterTest.java | 45 +++++ 12 files changed, 556 insertions(+), 119 deletions(-) create mode 100644 gson/src/main/java/com/google/gson/Strictness.java diff --git a/Troubleshooting.md b/Troubleshooting.md index 2f9185a38e..4f88ad6a0a 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -129,7 +129,7 @@ Notes: **Solution:** See [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html) section "Lenient JSON handling" -Note: Even in non-lenient mode Gson deviates slightly from the JSON specification, see [`JsonReader.setLenient`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setLenient(boolean)) for more details. +Note: Even in non-lenient mode Gson deviates slightly from the JSON specification, see [`JsonReader.setStrictness`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setStrictness(Strictness)) for more details. ## `IllegalStateException`: "Expected ... but was ..." diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 45336a87e9..ec95b9975d 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -105,10 +105,15 @@ *

See the Gson User Guide * for a more complete set of examples.

* - *

Lenient JSON handling

+ *

JSON Strictness handling

* For legacy reasons most of the {@code Gson} methods allow JSON data which does not - * comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()} - * is used or not. If this behavior is not desired, the following workarounds can be used: + * comply with the JSON specification when the strictness is set to {@code null} (the default value). + * To specify the {@linkplain Strictness strictness} of a {@code Gson} instance, you should set it through + * {@link GsonBuilder#setStrictness(Strictness)}. If the strictness of a {@code Gson} instance is set to a not-null + * value, the strictness will always be enforced. + * + *

For older Gson versions which don't have the {@link Strictness} mode API the following + * workarounds can be used: * *

Serialization

*
    @@ -140,7 +145,7 @@ */ public final class Gson { static final boolean DEFAULT_JSON_NON_EXECUTABLE = false; - static final boolean DEFAULT_LENIENT = false; + static final Strictness DEFAULT_STRICTNESS = null; static final FormattingStyle DEFAULT_FORMATTING_STYLE = null; static final boolean DEFAULT_ESCAPE_HTML = true; static final boolean DEFAULT_SERIALIZE_NULLS = false; @@ -184,7 +189,7 @@ public final class Gson { final boolean generateNonExecutableJson; final boolean htmlSafe; final FormattingStyle formattingStyle; - final boolean lenient; + final Strictness strictness; final boolean serializeSpecialFloatingPointValues; final boolean useJdkUnsafe; final String datePattern; @@ -231,13 +236,14 @@ public final class Gson { *
  1. By default, Gson excludes transient or static fields from * consideration for serialization and deserialization. You can change this behavior through * {@link GsonBuilder#excludeFieldsWithModifiers(int...)}.
  2. + *
  3. The strictness is set to {@code null}.
  4. * */ public Gson() { this(Excluder.DEFAULT, DEFAULT_FIELD_NAMING_STRATEGY, Collections.>emptyMap(), DEFAULT_SERIALIZE_NULLS, DEFAULT_COMPLEX_MAP_KEYS, DEFAULT_JSON_NON_EXECUTABLE, DEFAULT_ESCAPE_HTML, - DEFAULT_FORMATTING_STYLE, DEFAULT_LENIENT, DEFAULT_SPECIALIZE_FLOAT_VALUES, + DEFAULT_FORMATTING_STYLE, DEFAULT_STRICTNESS, DEFAULT_SPECIALIZE_FLOAT_VALUES, DEFAULT_USE_JDK_UNSAFE, LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT, Collections.emptyList(), Collections.emptyList(), @@ -248,7 +254,7 @@ public Gson() { Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy, Map> instanceCreators, boolean serializeNulls, boolean complexMapKeySerialization, boolean generateNonExecutableGson, boolean htmlSafe, - FormattingStyle formattingStyle, boolean lenient, boolean serializeSpecialFloatingPointValues, + FormattingStyle formattingStyle, Strictness strictness, boolean serializeSpecialFloatingPointValues, boolean useJdkUnsafe, LongSerializationPolicy longSerializationPolicy, String datePattern, int dateStyle, int timeStyle, List builderFactories, @@ -265,7 +271,7 @@ public Gson() { this.generateNonExecutableJson = generateNonExecutableGson; this.htmlSafe = htmlSafe; this.formattingStyle = formattingStyle; - this.lenient = lenient; + this.strictness = strictness; this.serializeSpecialFloatingPointValues = serializeSpecialFloatingPointValues; this.useJdkUnsafe = useJdkUnsafe; this.longSerializationPolicy = longSerializationPolicy; @@ -822,24 +828,37 @@ public void toJson(Object src, Type typeOfSrc, Appendable writer) throws JsonIOE * Writes the JSON representation of {@code src} of type {@code typeOfSrc} to * {@code writer}. * - *

    The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided writer. The lenient mode setting - * of the writer is restored once this method returns. +

    If the {@code Gson} instance has a not-null strictness setting, this setting will be used for reading the JSON + * regardless of the {@linkplain JsonReader#getStrictness() strictness} of the provided {@link JsonReader}. For legacy + * reasons, if the {@code Gson} instance has {@code null} as its strictness setting and the provided {@link JsonReader} + * has a strictness of {@link Strictness#LEGACY_STRICT}, the JSON will be read in {@linkplain Strictness#LENIENT} + * mode. Note that in both cases the old strictness value of the reader will be restored when this method returns. * *

    The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance * (configured by the {@link GsonBuilder}) are applied, and the original settings of the * writer are restored once this method returns. * + * @param src the object to be written. + * @param typeOfSrc the type of the object to be written. + * @param writer the {@link JsonWriter} writer to which the provided object will be written. + * * @throws JsonIOException if there was a problem writing to the writer */ public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOException { @SuppressWarnings("unchecked") TypeAdapter adapter = (TypeAdapter) getAdapter(TypeToken.get(typeOfSrc)); - boolean oldLenient = writer.isLenient(); - writer.setLenient(true); + + Strictness oldStrictness = writer.getStrictness(); + if (this.strictness != null) { + writer.setStrictness(this.strictness); + } else if (writer.getStrictness() == Strictness.LEGACY_STRICT){ + writer.setStrictness(Strictness.LENIENT); + } + boolean oldHtmlSafe = writer.isHtmlSafe(); - writer.setHtmlSafe(htmlSafe); boolean oldSerializeNulls = writer.getSerializeNulls(); + + writer.setHtmlSafe(htmlSafe); writer.setSerializeNulls(serializeNulls); try { adapter.write(writer, src); @@ -848,7 +867,7 @@ public void toJson(Object src, Type typeOfSrc, JsonWriter writer) throws JsonIOE } catch (AssertionError e) { throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e); } finally { - writer.setLenient(oldLenient); + writer.setStrictness(oldStrictness); writer.setHtmlSafe(oldHtmlSafe); writer.setSerializeNulls(oldSerializeNulls); } @@ -892,7 +911,9 @@ public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOExce *
  5. {@link GsonBuilder#disableHtmlEscaping()}
  6. *
  7. {@link GsonBuilder#generateNonExecutableJson()}
  8. *
  9. {@link GsonBuilder#serializeNulls()}
  10. - *
  11. {@link GsonBuilder#setLenient()}
  12. + *
  13. {@link GsonBuilder#setStrictness(Strictness)}. If the strictness of this {@code Gson} instance + * is set to {@code null}, the created writer will have a strictness of {@link Strictness#LEGACY_STRICT}. + * If the strictness is set to a non-null value, this strictness will be used for the created writer.
  14. *
  15. {@link GsonBuilder#setPrettyPrinting()}
  16. *
  17. {@link GsonBuilder#setPrettyPrinting(FormattingStyle)}
  18. * @@ -904,7 +925,7 @@ public JsonWriter newJsonWriter(Writer writer) throws IOException { JsonWriter jsonWriter = new JsonWriter(writer); jsonWriter.setFormattingStyle(formattingStyle); jsonWriter.setHtmlSafe(htmlSafe); - jsonWriter.setLenient(lenient); + jsonWriter.setStrictness(strictness == null ? Strictness.LEGACY_STRICT : strictness); jsonWriter.setSerializeNulls(serializeNulls); return jsonWriter; } @@ -914,35 +935,48 @@ public JsonWriter newJsonWriter(Writer writer) throws IOException { * *

    The following settings are considered: *

      - *
    • {@link GsonBuilder#setLenient()}
    • + *
    • {@link GsonBuilder#setStrictness(Strictness)}. If the strictness of this {@code Gson} instance + * is set to {@code null}, the created reader will have a strictness of {@link Strictness#LEGACY_STRICT}. + * If the strictness is set to a non-null value, this strictness will be used for the created reader.
    • *
    */ public JsonReader newJsonReader(Reader reader) { JsonReader jsonReader = new JsonReader(reader); - jsonReader.setLenient(lenient); + jsonReader.setStrictness(strictness == null ? Strictness.LEGACY_STRICT : strictness); return jsonReader; } /** * Writes the JSON for {@code jsonElement} to {@code writer}. * - *

    The JSON data is written in {@linkplain JsonWriter#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided writer. The lenient mode setting - * of the writer is restored once this method returns. + *

    If the {@code Gson} instance has a not-null strictness setting, this setting will be used for writing the JSON + * regardless of the {@linkplain JsonWriter#getStrictness() strictness} of the provided {@link JsonWriter}. For legacy + * reasons, if the {@code Gson} instance has {@code null} as its strictness setting and the provided {@link JsonWriter} + * has a strictness of {@link Strictness#LEGACY_STRICT}, the JSON will be written in {@linkplain Strictness#LENIENT} + * mode. Note that in both cases the old strictness value of the writer will be restored when this method returns. * *

    The 'HTML-safe' and 'serialize {@code null}' settings of this {@code Gson} instance * (configured by the {@link GsonBuilder}) are applied, and the original settings of the * writer are restored once this method returns. * + * @param jsonElement the JSON element to be written. + * @param writer the JSON writer to which the provided element will be written. * @throws JsonIOException if there was a problem writing to the writer */ public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOException { - boolean oldLenient = writer.isLenient(); - writer.setLenient(true); + Strictness oldStrictness = writer.getStrictness(); boolean oldHtmlSafe = writer.isHtmlSafe(); - writer.setHtmlSafe(htmlSafe); boolean oldSerializeNulls = writer.getSerializeNulls(); + + writer.setHtmlSafe(htmlSafe); writer.setSerializeNulls(serializeNulls); + + if (this.strictness != null) { + writer.setStrictness(this.strictness); + } else if (writer.getStrictness() == Strictness.LEGACY_STRICT) { + writer.setStrictness(Strictness.LENIENT); + } + try { Streams.write(jsonElement, writer); } catch (IOException e) { @@ -950,7 +984,7 @@ public void toJson(JsonElement jsonElement, JsonWriter writer) throws JsonIOExce } catch (AssertionError e) { throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e); } finally { - writer.setLenient(oldLenient); + writer.setStrictness(oldStrictness); writer.setHtmlSafe(oldHtmlSafe); writer.setSerializeNulls(oldSerializeNulls); } @@ -1169,8 +1203,8 @@ private static void assertFullConsumption(Object obj, JsonReader reader) { *

    Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has * multiple top-level JSON elements, or if there is trailing data. * - *

    The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided reader. The lenient mode setting + *

    The JSON data is parsed in {@linkplain JsonReader#setStrictness(Strictness) lenient mode}, + * regardless of the strictness setting of the provided reader. The strictness setting * of the reader is restored once this method returns. * * @param the type of the desired object @@ -1198,9 +1232,11 @@ public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, J *

    Unlike the other {@code fromJson} methods, no exception is thrown if the JSON data has * multiple top-level JSON elements, or if there is trailing data. * - *

    The JSON data is parsed in {@linkplain JsonReader#setLenient(boolean) lenient mode}, - * regardless of the lenient mode setting of the provided reader. The lenient mode setting - * of the reader is restored once this method returns. + *

    If the {@code Gson} instance has a not-null strictness setting, this setting will be used for reading the JSON + * regardless of the {@linkplain JsonReader#getStrictness() strictness} of the provided {@link JsonReader}. For legacy + * reasons, if the {@code Gson} instance has {@code null} as its strictness setting and the provided {@link JsonReader} + * has a strictness of {@link Strictness#LEGACY_STRICT}, the JSON will be read in {@linkplain Strictness#LENIENT} + * mode. Note that in both cases the old strictness value of the reader will be restored when this method returns. * * @param the type of the desired object * @param reader the reader whose next JSON value should be deserialized @@ -1220,8 +1256,14 @@ public T fromJson(JsonReader reader, Type typeOfT) throws JsonIOException, J */ public T fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOException, JsonSyntaxException { boolean isEmpty = true; - boolean oldLenient = reader.isLenient(); - reader.setLenient(true); + Strictness oldStrictness = reader.getStrictness(); + + if (this.strictness != null) { + reader.setStrictness(this.strictness); + } else if (reader.getStrictness() == Strictness.LEGACY_STRICT){ + reader.setStrictness(Strictness.LENIENT); + } + try { reader.peek(); isEmpty = false; @@ -1244,7 +1286,7 @@ public T fromJson(JsonReader reader, TypeToken typeOfT) throws JsonIOExce } catch (AssertionError e) { throw new AssertionError("AssertionError (GSON " + GsonBuildConfig.VERSION + "): " + e.getMessage(), e); } finally { - reader.setLenient(oldLenient); + reader.setStrictness(oldStrictness); } } diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 0afc2337c1..7540cda5aa 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -21,11 +21,11 @@ import static com.google.gson.Gson.DEFAULT_ESCAPE_HTML; import static com.google.gson.Gson.DEFAULT_FORMATTING_STYLE; import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE; -import static com.google.gson.Gson.DEFAULT_LENIENT; import static com.google.gson.Gson.DEFAULT_NUMBER_TO_NUMBER_STRATEGY; import static com.google.gson.Gson.DEFAULT_OBJECT_TO_NUMBER_STRATEGY; import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS; import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES; +import static com.google.gson.Gson.DEFAULT_STRICTNESS; import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE; import com.google.gson.annotations.Since; @@ -50,6 +50,7 @@ import java.util.Map; import java.util.Objects; + /** *

    Use this builder to construct a {@link Gson} instance when you need to set configuration * options other than the default. For {@link Gson} with default configuration, it is simpler to @@ -100,7 +101,7 @@ public final class GsonBuilder { private boolean escapeHtmlChars = DEFAULT_ESCAPE_HTML; private FormattingStyle formattingStyle = DEFAULT_FORMATTING_STYLE; private boolean generateNonExecutableJson = DEFAULT_JSON_NON_EXECUTABLE; - private boolean lenient = DEFAULT_LENIENT; + private Strictness strictness = DEFAULT_STRICTNESS; private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE; private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY; private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY; @@ -130,7 +131,7 @@ public GsonBuilder() { this.generateNonExecutableJson = gson.generateNonExecutableJson; this.escapeHtmlChars = gson.htmlSafe; this.formattingStyle = gson.formattingStyle; - this.lenient = gson.lenient; + this.strictness = gson.strictness; this.serializeSpecialFloatingPointValues = gson.serializeSpecialFloatingPointValues; this.longSerializationPolicy = gson.longSerializationPolicy; this.datePattern = gson.datePattern; @@ -502,17 +503,38 @@ public GsonBuilder setPrettyPrinting(FormattingStyle formattingStyle) { } /** - * Configures Gson to allow JSON data which does not strictly comply with the JSON specification. + * Sets the strictness of this builder to {@link Strictness#LENIENT}. * - *

    Note: Due to legacy reasons most methods of Gson are always lenient, regardless of - * whether this builder method is used. + *

    This method has been deprecated. Please use {@link GsonBuilder#setStrictness(Strictness)} instead. + * Calling this method is equivalent to {@code setStrictness(Strictness.LENIENT)}

    * - * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern - * @see JsonReader#setLenient(boolean) - * @see JsonWriter#setLenient(boolean) + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern. + * @see JsonReader#setStrictness(Strictness) + * @see JsonWriter#setStrictness(Strictness) + * @see #setStrictness(Strictness) */ + @Deprecated public GsonBuilder setLenient() { - lenient = true; + strictness = Strictness.LENIENT; + return this; + } + + /** + * Sets the strictness of this builder to the provided parameter. + * + *

    This changes how strict the + * RFC 8259 JSON specification is enforced when parsing or + * writing JSON. For details on this, refer to {@link JsonReader#setStrictness(Strictness)} and + * {@link JsonWriter#setStrictness(Strictness)}.

    + * + * @param strictness the new strictness mode. May not be {@code null}. + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern. + * @see JsonWriter#setStrictness(Strictness) + * @see JsonReader#setStrictness(Strictness) + * @since $next-version$ + */ + public GsonBuilder setStrictness(Strictness strictness) { + this.strictness = Objects.requireNonNull(strictness); return this; } @@ -684,7 +706,7 @@ public GsonBuilder registerTypeHierarchyAdapter(Class baseType, Object typeAd } /** - * Section 2.4 of JSON specification disallows + * Section 6 of JSON specification disallows * special double values (NaN, Infinity, -Infinity). However, * Javascript * specification (see section 4.3.20, 4.3.22, 4.3.23) allows these values as valid Javascript @@ -774,7 +796,7 @@ public Gson create() { return new Gson(excluder, fieldNamingPolicy, new HashMap<>(instanceCreators), serializeNulls, complexMapKeySerialization, - generateNonExecutableJson, escapeHtmlChars, formattingStyle, lenient, + generateNonExecutableJson, escapeHtmlChars, formattingStyle, strictness, serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy, datePattern, dateStyle, timeStyle, new ArrayList<>(this.factories), new ArrayList<>(this.hierarchyFactories), factories, diff --git a/gson/src/main/java/com/google/gson/Strictness.java b/gson/src/main/java/com/google/gson/Strictness.java new file mode 100644 index 0000000000..daf086a05f --- /dev/null +++ b/gson/src/main/java/com/google/gson/Strictness.java @@ -0,0 +1,29 @@ +package com.google.gson; + +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.JsonReader; + +/** + * Modes that indicate how strictly a JSON {@linkplain JsonReader reader} or + * {@linkplain JsonWriter writer} follows the syntax laid out in the + * RFC 8259 JSON specification. + * + *

    You can look at {@link JsonWriter#setStrictness(Strictness)} to see how the strictness + * affects the {@link JsonWriter} and you can look at + * {@link JsonReader#setStrictness(Strictness)} to see how the strictness + * affects the {@link JsonReader}.

    + * + * @see JsonReader#setStrictness(Strictness) + * @see JsonWriter#setStrictness(Strictness) + * @since $next-version$ + */ +public enum Strictness { + /** Allow large deviations from the JSON specification. */ + LENIENT, + + /** Allow certain small deviations from the JSON specification for legacy reasons. */ + LEGACY_STRICT, + + /** Strict compliance with the JSON specification. */ + STRICT +} 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 559ab2db81..df0e11934f 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -16,6 +16,7 @@ package com.google.gson.stream; +import com.google.gson.Strictness; import com.google.gson.internal.JsonReaderInternalAccess; import com.google.gson.internal.bind.JsonTreeReader; import java.io.Closeable; @@ -26,7 +27,7 @@ import java.util.Objects; /** - * Reads a JSON (RFC 7159) + * Reads a JSON (RFC 8259) * encoded value as a stream of tokens. This stream includes both literal * values (strings, numbers, booleans, and nulls) as well as the begin and * end delimiters of objects and arrays. The tokens are traversed in @@ -227,7 +228,7 @@ public class JsonReader implements Closeable { private final Reader in; /** True to accept non-spec compliant JSON */ - private boolean lenient = false; + private Strictness strictness = Strictness.LEGACY_STRICT; static final int BUFFER_SIZE = 1024; /** @@ -292,55 +293,102 @@ public JsonReader(Reader in) { } /** - * Configure this parser to be liberal in what it accepts. By default, - * this parser is strict and only accepts JSON as specified by RFC 4627. Setting the - * parser to lenient causes it to ignore the following syntax errors: + * Sets the strictness of this reader. * - *
      - *
    • Streams that start with the non-execute - * prefix, ")]}'\n". - *
    • Streams that include multiple top-level values. With strict parsing, - * each stream must contain exactly one top-level value. - *
    • Numbers may be {@link Double#isNaN() NaNs} or {@link - * Double#isInfinite() infinities}. - *
    • End of line comments starting with {@code //} or {@code #} and - * ending with a newline character. - *
    • C-style comments starting with {@code /*} and ending with - * {@code *}{@code /}. Such comments may not be nested. - *
    • Names that are unquoted or {@code 'single quoted'}. - *
    • Strings that are unquoted or {@code 'single quoted'}. - *
    • Array elements separated by {@code ;} instead of {@code ,}. - *
    • Unnecessary array separators. These are interpreted as if null - * was the omitted value. - *
    • Names and values separated by {@code =} or {@code =>} instead of - * {@code :}. - *
    • Name/value pairs separated by {@code ;} instead of {@code ,}. - *
    + *

    This method is deprecated. Please use {@link JsonReader#setStrictness(Strictness)} instead. + * {@code JsonReader.setLenient(true)} should be replaced by {@code JsonReader.setStrictness(Strictness.LENIENT)} + * and {@code JsonReader.setLenient(false)} should be replaced by {@code JsonReader.setStrictness(Strictness.LEGACY_STRICT)}. * - *

    Note: Even in strict mode there are slight derivations from the JSON - * specification: - *

      - *
    • JsonReader allows the literals {@code true}, {@code false} and {@code null} - * to have any capitalization, for example {@code fAlSe} - *
    • JsonReader supports the escape sequence {@code \'}, representing a {@code '} - *
    • JsonReader supports the escape sequence \LF (with {@code LF} - * being the Unicode character U+000A), resulting in a {@code LF} within the - * read JSON string - *
    • JsonReader allows unescaped control characters (U+0000 through U+001F) - *
    + * @param lenient whether this reader should be lenient. If true, the strictness is set to {@link Strictness#LENIENT}. + * If false, the strictness is set to {@link Strictness#LEGACY_STRICT}. + * @see #setStrictness(Strictness) */ + @Deprecated public final void setLenient(boolean lenient) { - this.lenient = lenient; + this.strictness = lenient ? Strictness.LENIENT : Strictness.LEGACY_STRICT; } /** - * Returns true if this parser is liberal in what it accepts. + * Returns true if the {@link Strictness} of this reader is equal to {@link Strictness#LENIENT}. + * + * @see #setStrictness(Strictness) */ public final boolean isLenient() { - return lenient; + return strictness == Strictness.LENIENT; } + /** + * Configures how liberal this parser is in what it accepts. + * + *

    In {@linkplain Strictness#STRICT strict} mode, the + * parser only accepts JSON in accordance with RFC 8259. + * In {@linkplain Strictness#LEGACY_STRICT legacy strict} mode, only JSON in accordance with the RFC 8259 is accepted, with a few exceptions denoted below + * for backwards compatibility reasons. In {@linkplain Strictness#LENIENT lenient} mode, + * all sort of non-spec compliant JSON is accepted (see below).

    + * + *
    + *
    {@link Strictness#STRICT}
    + *
    + * In strict mode, only input compliant with RFC 8259 + * is accepted. + *
    + *
    {@link Strictness#LEGACY_STRICT}
    + *
    + * In legacy strict mode, the following departures from RFC 8259 + * are accepted: + *
      + *
    • JsonReader allows the literals {@code true}, {@code false} and {@code null} + * to have any capitalization, for example {@code fAlSe} or {@code NULL}. + *
    • JsonReader supports the escape sequence {@code \'}, representing a {@code '} (single-quote) + *
    • JsonReader supports the escape sequence \LF (with {@code LF} + * being the Unicode character {@code U+000A}), resulting in a {@code LF} within the + * read JSON string + *
    • JsonReader allows unescaped control characters ({@code U+0000} through {@code U+001F}) + *
    + *
    + *
    {@link Strictness#LENIENT}
    + *
    + * In lenient mode, all input that is accepted in legacy strict mode is accepted in addition to the following + * departures from RFC 8259: + *
      + *
    • Streams that start with the non-execute prefix, {@code ")]}'\n"} + *
    • Streams that include multiple top-level values. With legacy strict or strict parsing, + * each stream must contain exactly one top-level value. + *
    • Numbers may be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() infinities} represented by + * {@code NaN} and {@code (-)Infinity} respectively. + *
    • End of line comments starting with {@code //} or {@code #} and ending with a newline character. + *
    • C-style comments starting with {@code /*} and ending with + * {@code *}{@code /}. Such comments may not be nested. + *
    • Names that are unquoted or {@code 'single quoted'}. + *
    • Strings that are unquoted or {@code 'single quoted'}. + *
    • Array elements separated by {@code ;} instead of {@code ,}. + *
    • Unnecessary array separators. These are interpreted as if null + * was the omitted value. + *
    • Names and values separated by {@code =} or {@code =>} instead of + * {@code :}. + *
    • Name/value pairs separated by {@code ;} instead of {@code ,}. + *
    + *
    + *
    + * + * @param strictness the new strictness value of this reader. May not be {@code null}. + * @since $next-version$ + */ + public final void setStrictness(Strictness strictness) { + Objects.requireNonNull(strictness); + this.strictness = strictness; + } + + /** + * Returns the {@linkplain Strictness strictness} of this reader. + * + * @see #setStrictness(Strictness) + * @since $next-version$ + */ + public final Strictness getStrictness() { + return strictness; + } /** * Consumes the next token from the JSON stream and asserts that it is the * beginning of a new array. @@ -539,7 +587,7 @@ int doPeek() throws IOException { throw syntaxError("Expected ':'"); } } else if (peekStack == JsonScope.EMPTY_DOCUMENT) { - if (lenient) { + if (strictness == Strictness.LENIENT) { consumeNonExecutePrefix(); } stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; @@ -609,6 +657,8 @@ private int peekKeyword() throws IOException { String keyword; String keywordUpper; int peeking; + + // Look at the first lower case letter to determine what keyword we are trying to match. if (c == 't' || c == 'T') { keyword = "true"; keywordUpper = "TRUE"; @@ -625,14 +675,18 @@ private int peekKeyword() throws IOException { return PEEKED_NONE; } - // Confirm that chars [1..length) match the keyword. + // Upper cased keywords are not allowed in STRICT mode + boolean allowsUpperCased = strictness != Strictness.STRICT; + + // Confirm that chars [0..length) match the keyword. int length = keyword.length(); - for (int i = 1; i < length; i++) { + for (int i = 0; i < length; i++) { if (pos + i >= limit && !fillBuffer(i + 1)) { return PEEKED_NONE; } c = buffer[pos + i]; - if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) { + boolean matched = c == keyword.charAt(i) || (allowsUpperCased && c == keywordUpper.charAt(i)); + if (!matched) { return PEEKED_NONE; } } @@ -920,7 +974,7 @@ public double nextDouble() throws IOException { peeked = PEEKED_BUFFERED; double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException. - if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { + if (strictness != Strictness.LENIENT && (Double.isNaN(result) || Double.isInfinite(result))) { throw new MalformedJsonException( "JSON forbids NaN and infinities: " + result + locationString()); } @@ -1007,7 +1061,10 @@ private String nextQuotedValue(char quote) throws IOException { while (p < l) { int c = buffer[p++]; - if (c == quote) { + // In strict mode, throw an exception when meeting unescaped control characters (U+0000 through U+001F) + if (strictness == Strictness.STRICT && c < 0x20) { + throw syntaxError("Unescaped control characters (\\u0000-\\u001F) are not allowed in strict mode."); + } else if (c == quote) { pos = p; int len = p - start - 1; if (builder == null) { @@ -1461,7 +1518,7 @@ private int nextNonWhitespace(boolean throwOnEof) throws IOException { } private void checkLenient() throws IOException { - if (!lenient) { + if (strictness != Strictness.LENIENT) { throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON"); } } @@ -1636,11 +1693,17 @@ private char readEscapeCharacter() throws IOException { return '\f'; case '\n': + if (strictness == Strictness.STRICT) { + throw syntaxError("Cannot escape a newline character in strict mode!"); + } lineNumber++; lineStart = pos; // fall-through case '\'': + if (strictness == Strictness.STRICT) { + throw syntaxError("Invalid escaped character \"'\" in strict mode"); + } case '"': case '\\': case '/': 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 460dcce200..696f087eaf 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -24,6 +24,8 @@ import static com.google.gson.stream.JsonScope.NONEMPTY_DOCUMENT; import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT; +import com.google.gson.FormattingStyle; +import com.google.gson.Strictness; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; @@ -36,10 +38,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; -import com.google.gson.FormattingStyle; - /** - * Writes a JSON (RFC 7159) + * Writes a JSON (RFC 8259) * encoded value to a stream, one token at a time. The stream includes both * literal values (strings, numbers, booleans and nulls) as well as the begin * and end delimiters of objects and arrays. @@ -141,7 +141,7 @@ 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 7159, "All Unicode characters may be placed within the + * 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)." @@ -191,7 +191,7 @@ public class JsonWriter implements Closeable, Flushable { */ private String separator = ":"; - private boolean lenient; + private Strictness strictness = Strictness.LEGACY_STRICT; private boolean htmlSafe; @@ -228,7 +228,7 @@ public final void setIndent(String indent) { * Sets the pretty printing style to be used in the encoded document. * No pretty printing if null. * - *

    Sets the various attributes to be used in the encoded document. + *

    Sets the various attributes to be used in the encoded document. * For example the indentation string to be repeated for each level of indentation. * Or the newline style, to accommodate various OS styles.

    * @@ -257,28 +257,66 @@ public final FormattingStyle getFormattingStyle() { } /** - * Configure this writer to relax its syntax rules. By default, this writer - * only emits well-formed JSON as specified by RFC 7159. Setting the writer - * to lenient permits the following: - *
      - *
    • Numbers may be {@link Double#isNaN() NaNs} or {@link - * Double#isInfinite() infinities}. - *
    + * Sets the strictness of this writer. + * + *

    This method is deprecated. Please use {@link JsonWriter#setStrictness(Strictness)} instead. + * {@code JsonWriter.setLenient(true)} should be replaced by {@code JsonWriter.setStrictness(Strictness.LENIENT)} + * and {@code JsonWriter.setLenient(false)} should be replaced by {@code JsonWriter.setStrictness(Strictness.LEGACY_STRICT)}. + * + * @param lenient whether this writer should be lenient. If true, the strictness is set to {@link Strictness#LENIENT}. + * If false, the strictness is set to {@link Strictness#LEGACY_STRICT}. + * @see #setStrictness(Strictness) */ + @Deprecated public final void setLenient(boolean lenient) { - this.lenient = lenient; + this.strictness = lenient ? Strictness.LENIENT : Strictness.LEGACY_STRICT; } /** - * Returns true if this writer has relaxed syntax rules. + * Returns true if the {@link Strictness} of this writer is equal to {@link Strictness#LENIENT}. + * + * @see JsonWriter#setStrictness(Strictness) */ public boolean isLenient() { - return lenient; + return strictness == Strictness.LENIENT; + } + + /** + * Configures how strict this writer is with regard to the syntax rules specified in RFC 8259. By default, {@link Strictness#LEGACY_STRICT} is used. + * + *

    + *
    {@link Strictness#STRICT} & {@link Strictness#LEGACY_STRICT}
    + *
    + * The behavior of these is currently identical. In these strictness modes, the writer only writes JSON + * in accordance with RFC 8259. + *
    + *
    {@link Strictness#LENIENT}
    + *
    + * This mode relaxes the behavior of the writer to allow the writing of {@link Double#isNaN() NaNs} + * and {@link Double#isInfinite() infinities}. It also allows writing multiple top level values. + *
    + *
    + * + * @param strictness the new strictness of this writer. May not be {@code null}. + * @since $next-version$ + */ + public final void setStrictness(Strictness strictness) { + this.strictness = Objects.requireNonNull(strictness); + } + + /** + * Returns how strict this writer is. + * + * @see #setStrictness(Strictness) + * @since $next-version$ + */ + public final Strictness getStrictness() { + return strictness; } /** - * Configure this writer to emit JSON that's safe for direct inclusion in HTML + * Configures this writer to emit JSON that's safe for direct inclusion in HTML * and XML documents. This escapes the HTML characters {@code <}, {@code >}, * {@code &} and {@code =} before writing them to the stream. Without this * setting, your XML/HTML encoder should replace these characters with the @@ -532,7 +570,7 @@ public JsonWriter value(Boolean value) throws IOException { */ public JsonWriter value(float value) throws IOException { writeDeferredName(); - if (!lenient && (Float.isNaN(value) || Float.isInfinite(value))) { + if (strictness != Strictness.LENIENT && (Float.isNaN(value) || Float.isInfinite(value))) { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } beforeValue(); @@ -551,7 +589,7 @@ public JsonWriter value(float value) throws IOException { */ public JsonWriter value(double value) throws IOException { writeDeferredName(); - if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) { + if (strictness != Strictness.LENIENT && (Double.isNaN(value) || Double.isInfinite(value))) { throw new IllegalArgumentException("Numeric values must be finite, but was " + value); } beforeValue(); @@ -601,7 +639,7 @@ public JsonWriter value(Number value) throws IOException { writeDeferredName(); String string = value.toString(); if (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN")) { - if (!lenient) { + if (strictness != Strictness.LENIENT) { throw new IllegalArgumentException("Numeric values must be finite, but was " + string); } } else { @@ -710,7 +748,7 @@ private void beforeName() throws IOException { private void beforeValue() throws IOException { switch (peek()) { case NONEMPTY_DOCUMENT: - if (!lenient) { + if (strictness != Strictness.LENIENT) { throw new IllegalStateException( "JSON must have only one top-level value."); } diff --git a/gson/src/test/java/com/google/gson/GsonBuilderTest.java b/gson/src/test/java/com/google/gson/GsonBuilderTest.java index 4c6d5ec91e..3b5f9b027f 100644 --- a/gson/src/test/java/com/google/gson/GsonBuilderTest.java +++ b/gson/src/test/java/com/google/gson/GsonBuilderTest.java @@ -22,6 +22,8 @@ import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.Type; @@ -224,4 +226,31 @@ public void testSetVersionInvalid() { assertThat(e).hasMessageThat().isEqualTo("Invalid version: -0.1"); } } + + @Test + public void testDefaultStrictness() throws IOException { + GsonBuilder builder = new GsonBuilder(); + Gson gson = builder.create(); + assertThat(gson.newJsonReader(new StringReader("{}}")).getStrictness()).isEqualTo(Strictness.LEGACY_STRICT); + assertThat(gson.newJsonWriter(new StringWriter()).getStrictness()).isEqualTo(Strictness.LEGACY_STRICT); + } + + @Test + public void testSetLenient() throws IOException { + GsonBuilder builder = new GsonBuilder(); + builder.setLenient(); + Gson gson = builder.create(); + assertThat(gson.newJsonReader(new StringReader("{}}")).getStrictness()).isEqualTo(Strictness.LENIENT); + assertThat(gson.newJsonWriter(new StringWriter()).getStrictness()).isEqualTo(Strictness.LENIENT); + } + + @Test + public void testSetStrictness() throws IOException { + final Strictness STRICTNESS = Strictness.STRICT; + GsonBuilder builder = new GsonBuilder(); + builder.setStrictness(STRICTNESS); + Gson gson = builder.create(); + assertThat(gson.newJsonReader(new StringReader("{}}")).getStrictness()).isEqualTo(STRICTNESS); + assertThat(gson.newJsonWriter(new StringWriter()).getStrictness()).isEqualTo(STRICTNESS); + } } diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index c1e9e9d785..88fabde393 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -59,11 +59,16 @@ public final class GsonTest { private static final ToNumberStrategy CUSTOM_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE; private static final ToNumberStrategy CUSTOM_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER; + @Test + public void testStrictnessDefault() { + assertThat(new Gson().strictness).isNull(); + } + @Test public void testOverridesDefaultExcluder() { Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, - FormattingStyle.DEFAULT, true, false, true, + FormattingStyle.DEFAULT, Strictness.LENIENT, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), @@ -80,7 +85,7 @@ public void testOverridesDefaultExcluder() { public void testClonedTypeAdapterFactoryListsAreIndependent() { Gson original = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, - FormattingStyle.DEFAULT, true, false, true, + FormattingStyle.DEFAULT, Strictness.LENIENT, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java index 5faa718015..337d09a5e8 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java @@ -144,7 +144,7 @@ public JsonElement deepCopy() { */ @Test public void testOverrides() { - List ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()"); + List ignoredMethods = Arrays.asList("setLenient(boolean)", "isLenient()", "setStrictness(com.google.gson.Strictness)", "getStrictness()"); MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods); } } diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java index 116d275c0d..5dc9435682 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java @@ -280,6 +280,7 @@ public void testJsonValue() throws IOException { public void testOverrides() { List ignoredMethods = Arrays.asList( "setLenient(boolean)", "isLenient()", + "setStrictness(com.google.gson.Strictness)", "getStrictness()", "setIndent(java.lang.String)", "setHtmlSafe(boolean)", "isHtmlSafe()", "setFormattingStyle(com.google.gson.FormattingStyle)", "getFormattingStyle()", 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 8ebe20b571..b0eb2e282a 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -27,17 +27,132 @@ import static com.google.gson.stream.JsonToken.STRING; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import static org.junit.Assert.assertThrows; import java.io.EOFException; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.Arrays; +import com.google.gson.Strictness; import org.junit.Ignore; import org.junit.Test; @SuppressWarnings("resource") public final class JsonReaderTest { + + @Test + public void testSetLenientTrue() { + JsonReader reader = new JsonReader(reader("{}")); + reader.setLenient(true); + assertThat(reader.getStrictness()).isEqualTo(Strictness.LENIENT); + } + + @Test + public void testSetLenientFalse() { + JsonReader reader = new JsonReader(reader("{}")); + reader.setLenient(false); + assertThat(reader.getStrictness()).isEqualTo(Strictness.LEGACY_STRICT); + } + + @Test + public void testSetStrictness() { + JsonReader reader = new JsonReader(reader("{}")); + reader.setStrictness(Strictness.STRICT); + assertThat(reader.getStrictness()).isEqualTo(Strictness.STRICT); + } + + @Test + public void testSetStrictnessNull() { + JsonReader reader = new JsonReader(reader("{}")); + assertThrows(NullPointerException.class, () -> reader.setStrictness(null)); + } + + @Test + public void testEscapedNewlineNotAllowedInStrictMode() throws IOException { + String json = "\"\\\n\""; + JsonReader reader = new JsonReader(reader(json)); + reader.setStrictness(Strictness.STRICT); + + IOException expected = assertThrows(IOException.class, reader::nextString); + assertThat(expected.getMessage()).contains("Cannot escape a newline character in strict mode!"); + } + + @Test + public void testEscapedNewlineAllowedInDefaultMode() throws IOException { + String json = "\"\\\n\""; + JsonReader reader = new JsonReader(reader(json)); + assertThat(reader.nextString()).isEqualTo("\n"); + } + + @Test + public void testStrictModeFailsToParseUnescapedControlCharacter() { + String json = "\"\t\""; + JsonReader reader = new JsonReader(reader(json)); + reader.setStrictness(Strictness.STRICT); + + IOException expected = assertThrows(IOException.class, reader::nextString); + assertThat(expected.getMessage()).contains("Unescaped control characters (\\u0000-\\u001F) are not allowed in strict mode."); + } + + @Test + public void testNonStrictModeParsesUnescapedControlCharacter() throws IOException { + String json = "\"\t\""; + JsonReader reader = new JsonReader(reader(json)); + assertThat(reader.nextString()).isEqualTo("\t"); + } + + @Test + public void testCapitalizedTrueFailWhenStrict() throws IOException { + JsonReader reader = new JsonReader(reader("TRUE")); + reader.setStrictness(Strictness.STRICT); + + IOException expected = assertThrows(IOException.class, reader::nextString); + assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed" + + " JSON at line 1 column 1 path $"); + + reader = new JsonReader(reader("True")); + reader.setStrictness(Strictness.STRICT); + + expected = assertThrows(IOException.class, reader::nextString); + assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed" + + " JSON at line 1 column 1 path $"); + } + + @Test + public void testCapitalizedNullFailWhenStrict() throws IOException { + JsonReader reader = new JsonReader(reader("NULL")); + reader.setStrictness(Strictness.STRICT); + + IOException expected = assertThrows(IOException.class, reader::nextNull); + assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed" + + " JSON at line 1 column 1 path $"); + + reader = new JsonReader(reader("nulL")); + reader.setStrictness(Strictness.STRICT); + + expected = assertThrows(IOException.class, reader::nextNull); + assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed" + + " JSON at line 1 column 1 path $"); + } + + @Test + public void testCapitalizedFalseFailWhenStrict() throws IOException { + JsonReader reader = new JsonReader(reader("FALSE")); + reader.setStrictness(Strictness.STRICT); + + IOException expected = assertThrows(IOException.class, reader::nextBoolean); + assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed" + + " JSON at line 1 column 1 path $"); + + reader = new JsonReader(reader("FaLse")); + reader.setStrictness(Strictness.STRICT); + + expected = assertThrows(IOException.class, reader::nextBoolean); + assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed" + + " JSON at line 1 column 1 path $"); + } + @Test public void testReadArray() throws IOException { JsonReader reader = new JsonReader(reader("[true, true]")); @@ -347,6 +462,43 @@ public void testCharacterUnescaping() throws IOException { assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); } + @Test + public void testReaderDoesNotTreatU2028U2029AsNewline() throws IOException { + // This test shows that the JSON String [\n"whatever'] is seen as valid + // And the JSON string [\u2028"whatever"] is not. + String jsonInvalid2028 = "[\u2028\"whatever\"]"; + JsonReader readerInvalid2028 = new JsonReader((reader(jsonInvalid2028))); + readerInvalid2028.beginArray(); + assertThrows(IOException.class, readerInvalid2028::nextString); + + String jsonInvalid2029 = "[\u2029\"whatever\"]"; + JsonReader readerInvalid2029 = new JsonReader((reader(jsonInvalid2029))); + readerInvalid2029.beginArray(); + assertThrows(IOException.class, readerInvalid2029::nextString); + + String jsonValid = "[\n\"whatever\"]"; + JsonReader readerValid = new JsonReader(reader(jsonValid)); + readerValid.beginArray(); + assertThat(readerValid.nextString()).isEqualTo("whatever"); + } + + @Test + public void testEscapeCharacterQuoteInStrictMode() throws IOException { + String json = "\"\\'\""; + JsonReader reader = new JsonReader(reader(json)); + reader.setStrictness(Strictness.STRICT); + + IOException expected = assertThrows(IOException.class, reader::nextString); + assertThat(expected).hasMessageThat().contains("Invalid escaped character \"'\" in strict mode"); + } + + @Test + public void testEscapeCharacterQuoteWithoutStrictMode() throws IOException { + String json = "\"\\'\""; + JsonReader reader = new JsonReader(reader(json)); + assertThat(reader.nextString()).isEqualTo("'"); + } + @Test public void testUnescapingInvalidCharacters() throws IOException { String json = "[\"\\u000g\"]"; @@ -359,6 +511,17 @@ public void testUnescapingInvalidCharacters() throws IOException { } } + @Test + public void testUnescapedControlCharactersInStrictMode() throws IOException { + String json = "[\"\u0014\"]"; + JsonReader reader = new JsonReader(reader(json)); + reader.setStrictness(Strictness.STRICT); + reader.beginArray(); + + IOException expected = assertThrows(IOException.class, reader::nextString); + assertThat(expected).hasMessageThat().contains("Unescaped control characters"); + } + @Test public void testUnescapingTruncatedCharacters() throws IOException { String json = "[\"\\u000"; 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 70470a166b..6ecdde74a7 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -17,9 +17,11 @@ package com.google.gson.stream; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; import com.google.gson.FormattingStyle; +import com.google.gson.Strictness; import com.google.gson.internal.LazilyParsedNumber; import java.io.IOException; import java.io.StringWriter; @@ -30,6 +32,49 @@ @SuppressWarnings("resource") public final class JsonWriterTest { + @Test + public void testDefaultStrictness() throws IOException { + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.LEGACY_STRICT); + jsonWriter.value(false); + jsonWriter.close(); + } + + @Test + public void testSetLenientTrue() throws IOException { + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + jsonWriter.setLenient(true); + assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.LENIENT); + jsonWriter.value(false); + jsonWriter.close(); + } + + @Test + public void testSetLenientFalse() throws IOException { + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + jsonWriter.setLenient(false); + assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.LEGACY_STRICT); + jsonWriter.value(false); + jsonWriter.close(); + } + + @Test + public void testSetStrictness() throws IOException { + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + jsonWriter.setStrictness(Strictness.STRICT); + assertThat(jsonWriter.getStrictness()).isEqualTo(Strictness.STRICT); + jsonWriter.value(false); + jsonWriter.close(); + } + + @Test + public void testSetStrictnessNull() throws IOException { + JsonWriter jsonWriter = new JsonWriter(new StringWriter()); + assertThrows(NullPointerException.class, () -> jsonWriter.setStrictness(null)); + jsonWriter.value(false); + jsonWriter.close(); + } + @Test public void testTopLevelValueTypes() throws IOException { StringWriter string1 = new StringWriter();