From 00d61b96dff050ec4b061bead047239b21a48764 Mon Sep 17 00:00:00 2001 From: Tomo Suzuki Date: Thu, 23 Feb 2023 21:27:50 -0500 Subject: [PATCH] feat: GsonFactory to have read leniency option (#1819) * feat: GsonFactory to have read leniency option --- .../api/client/json/gson/GsonFactory.java | 43 +++++++++++++++++++ .../api/client/json/gson/GsonParser.java | 2 +- .../api/client/json/gson/GsonFactoryTest.java | 32 ++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java index f02ba0f30..6c89dd16e 100644 --- a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java +++ b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonFactory.java @@ -52,12 +52,23 @@ public static GsonFactory getDefaultInstance() { return InstanceHolder.INSTANCE; } + /** Controls the behavior of leniency in reading JSON value */ + private boolean readLeniency = false; + /** Holder for the result of {@link #getDefaultInstance()}. */ @Beta static class InstanceHolder { static final GsonFactory INSTANCE = new GsonFactory(); } + // Keeping the default, non-arg constructor for backward compatibility. Users should use + // getDefaultInstance() or builder() instead. + public GsonFactory() {} + + private GsonFactory(Builder builder) { + readLeniency = builder.readLeniency; + } + @Override public JsonParser createJsonParser(InputStream in) { return createJsonParser(new InputStreamReader(in, StandardCharsets.UTF_8)); @@ -90,4 +101,36 @@ public JsonGenerator createJsonGenerator(OutputStream out, Charset enc) { public JsonGenerator createJsonGenerator(Writer writer) { return new GsonGenerator(this, new JsonWriter(writer)); } + + /** Returns true if it is lenient to input JSON value. */ + boolean getReadLeniency() { + return readLeniency; + } + + /** Returns the builder * */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for GsonFactory. */ + public static final class Builder { + // Do not directly call this constructor + private Builder() {} + + private boolean readLeniency = false; + + /** + * Set to {@code true} when you want to the JSON parser to be lenient to reading JSON value. By + * default, it is {@code false}. + */ + public Builder setReadLeniency(boolean readLeniency) { + this.readLeniency = readLeniency; + return this; + } + + /** Builds GsonFactory instance. */ + public GsonFactory build() { + return new GsonFactory(this); + } + } } diff --git a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java index dc90369c7..cee6fadc4 100644 --- a/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java +++ b/google-http-client-gson/src/main/java/com/google/api/client/json/gson/GsonParser.java @@ -43,7 +43,7 @@ class GsonParser extends JsonParser { GsonParser(GsonFactory factory, JsonReader reader) { this.factory = factory; this.reader = reader; - reader.setLenient(false); + reader.setLenient(factory.getReadLeniency()); } @Override diff --git a/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java b/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java index 05ab5bf04..5d166e689 100644 --- a/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java +++ b/google-http-client-gson/src/test/java/com/google/api/client/json/gson/GsonFactoryTest.java @@ -14,10 +14,16 @@ package com.google.api.client.json.gson; +import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; import com.google.api.client.json.JsonParser; import com.google.api.client.test.json.AbstractJsonFactoryTest; +import com.google.gson.stream.MalformedJsonException; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; /** @@ -94,4 +100,30 @@ public final void testGetByteValue() throws IOException { assertNotNull(ex.getMessage()); } } + + public final void testReaderLeniency_lenient() throws IOException { + JsonObjectParser parser = + new JsonObjectParser(GsonFactory.builder().setReadLeniency(true).build()); + + // This prefix in JSON body is used to prevent Cross-site script inclusion (XSSI). + InputStream inputStream = + new ByteArrayInputStream((")]}'\n" + JSON_ENTRY_PRETTY).getBytes(StandardCharsets.UTF_8)); + GenericJson json = parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); + + assertEquals("foo", json.get("title")); + } + + public final void testReaderLeniency_not_lenient_by_default() throws IOException { + JsonObjectParser parser = new JsonObjectParser(GsonFactory.getDefaultInstance()); + + try { + // This prefix in JSON body is used to prevent Cross-site script inclusion (XSSI). + InputStream inputStream = + new ByteArrayInputStream((")]}'\n" + JSON_ENTRY_PRETTY).getBytes(StandardCharsets.UTF_8)); + parser.parseAndClose(inputStream, StandardCharsets.UTF_8, GenericJson.class); + fail("The read leniency should fail the JSON input with XSSI prefix."); + } catch (MalformedJsonException ex) { + assertNotNull(ex.getMessage()); + } + } }