diff --git a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java index 90ee595782..98bf845d80 100644 --- a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java +++ b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java @@ -20,6 +20,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import com.google.gson.JsonElement; +import com.google.gson.ReflectionAccessFilter; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.internal.ConstructorConstructor; @@ -30,6 +31,7 @@ import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.lang.reflect.Type; +import java.util.Collections; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedList; @@ -47,7 +49,7 @@ public final class GraphAdapterBuilder { public GraphAdapterBuilder() { this.instanceCreators = new HashMap>(); - this.constructorConstructor = new ConstructorConstructor(instanceCreators, true); + this.constructorConstructor = new ConstructorConstructor(instanceCreators, true, Collections.emptyList()); } public GraphAdapterBuilder addType(Type type) { final ObjectConstructor objectConstructor = constructorConstructor.get(TypeToken.get(type)); diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index 62c8b0c6b7..225f7b99f3 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -156,6 +156,7 @@ public final class Gson { final List builderHierarchyFactories; final ToNumberStrategy objectToNumberStrategy; final ToNumberStrategy numberToNumberStrategy; + final List reflectionFilters; /** * Constructs a Gson object with default configuration. The default configuration has the @@ -199,7 +200,8 @@ public Gson() { DEFAULT_USE_JDK_UNSAFE, LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT, Collections.emptyList(), Collections.emptyList(), - Collections.emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY); + Collections.emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY, + Collections.emptyList()); } Gson(Excluder excluder, FieldNamingStrategy fieldNamingStrategy, @@ -211,11 +213,12 @@ public Gson() { int timeStyle, List builderFactories, List builderHierarchyFactories, List factoriesToBeAdded, - ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy) { + ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy, + List reflectionFilters) { this.excluder = excluder; this.fieldNamingStrategy = fieldNamingStrategy; this.instanceCreators = instanceCreators; - this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe); + this.constructorConstructor = new ConstructorConstructor(instanceCreators, useJdkUnsafe, reflectionFilters); this.serializeNulls = serializeNulls; this.complexMapKeySerialization = complexMapKeySerialization; this.generateNonExecutableJson = generateNonExecutableGson; @@ -232,6 +235,7 @@ public Gson() { this.builderHierarchyFactories = builderHierarchyFactories; this.objectToNumberStrategy = objectToNumberStrategy; this.numberToNumberStrategy = numberToNumberStrategy; + this.reflectionFilters = reflectionFilters; List factories = new ArrayList(); @@ -296,7 +300,7 @@ public Gson() { factories.add(jsonAdapterFactory); factories.add(TypeAdapters.ENUM_FACTORY); factories.add(new ReflectiveTypeAdapterFactory( - constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory)); + constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory, reflectionFilters)); this.factories = Collections.unmodifiableList(factories); } diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 43318fb3db..22935b1a2d 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -101,6 +102,7 @@ public final class GsonBuilder { private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE; private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY; private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY; + private final LinkedList reflectionFilters = new LinkedList(); /** * Creates a GsonBuilder instance that can be used to build Gson with various configuration @@ -137,6 +139,7 @@ public GsonBuilder() { this.useJdkUnsafe = gson.useJdkUnsafe; this.objectToNumberStrategy = gson.objectToNumberStrategy; this.numberToNumberStrategy = gson.numberToNumberStrategy; + this.reflectionFilters.addAll(gson.reflectionFilters); } /** @@ -632,6 +635,28 @@ public GsonBuilder disableJdkUnsafe() { return this; } + /** + * Adds a reflection access filter. A reflection access filter prevents Gson from using + * reflection for the serialization and deserialization of certain classes. The logic in + * the filter specifies which classes those are. + * + *

Filters will be invoked in reverse registration order, that is, the most recently + * added filter will be invoked first. + * + *

By default Gson has no filters configured and will try to use reflection for + * all classes for which no {@link TypeAdapter} has been registered, and for which no + * built-in Gson {@code TypeAdapter} exists. + * + * @param filter filter to add + * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern + */ + public GsonBuilder addReflectionAccessFilter(ReflectionAccessFilter filter) { + if (filter == null) throw new NullPointerException(); + + reflectionFilters.addFirst(filter); + return this; + } + /** * Creates a {@link Gson} instance based on the current configuration. This method is free of * side-effects to this {@code GsonBuilder} instance and hence can be called multiple times. @@ -649,12 +674,13 @@ public Gson create() { addTypeAdaptersForDate(datePattern, dateStyle, timeStyle, factories); - return new Gson(excluder, fieldNamingPolicy, instanceCreators, + return new Gson(excluder, fieldNamingPolicy, new HashMap>(instanceCreators), serializeNulls, complexMapKeySerialization, generateNonExecutableJson, escapeHtmlChars, prettyPrinting, lenient, serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy, - datePattern, dateStyle, timeStyle, - this.factories, this.hierarchyFactories, factories, objectToNumberStrategy, numberToNumberStrategy); + datePattern, dateStyle, timeStyle, new ArrayList(this.factories), + new ArrayList(this.hierarchyFactories), factories, + objectToNumberStrategy, numberToNumberStrategy, new ArrayList(reflectionFilters)); } private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle, diff --git a/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java new file mode 100644 index 0000000000..b787ae8942 --- /dev/null +++ b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java @@ -0,0 +1,194 @@ +package com.google.gson; + +import java.lang.reflect.AccessibleObject; + +import com.google.gson.internal.ReflectionAccessFilterHelper; + +/** + * Filter for determining whether reflection based serialization and + * deserialization is allowed for a class. + * + *

A filter can be useful in multiple scenarios, for example when + * upgrading to newer Java versions which use the Java Platform Module + * System (JPMS). A filter then allows to {@linkplain FilterResult#BLOCK_INACCESSIBLE + * prevent making inaccessible members accessible}, even if the used + * Java version might still allow illegal access (but logs a warning), + * or if {@code java} command line arguments are used to open the inaccessible + * packages to other parts of the application. This interface defines some + * convenience filters for this task, such as {@link #BLOCK_INACCESSIBLE_JAVA}. + * + *

A filter can also be useful to prevent mixing model classes of a + * project with other non-model classes; the filter could + * {@linkplain FilterResult#BLOCK_ALL block all reflective access} to + * non-model classes. + * + *

A reflection access filter is similar to an {@link ExclusionStrategy} + * with the major difference that a filter will cause an exception to be + * thrown when access is disallowed while an exclusion strategy just skips + * fields and classes. + * + * @see GsonBuilder#addReflectionAccessFilter(ReflectionAccessFilter) + */ +public interface ReflectionAccessFilter { + /** + * Result of a filter check. + */ + enum FilterResult { + /** + * Reflection access for the class is allowed. + * + *

Note that this does not affect the Java access checks in any way, + * it only permits Gson to try using reflection for a class. The Java + * runtime might still deny such access. + */ + ALLOW, + /** + * The filter is indecisive whether reflection access should be allowed. + * The next registered filter will be consulted to get the result. If + * there is no next filter, this result acts like {@link #ALLOW}. + */ + INDECISIVE, + /** + * Blocks reflection access if a member of the class is not accessible + * by default and would have to be made accessible. This is unaffected + * by any {@code java} command line arguments being used to make packages + * accessible, or by module declaration directives which open the + * complete module or certain packages for reflection and will consider + * such packages inaccessible. + * + *

Note that this only works for Java 9 and higher, for older + * Java versions its functionality will be limited and it might behave like + * {@link #ALLOW}. Access checks are only performed as defined by the Java + * Language Specification (JLS 11 §6.6), + * restrictions imposed by a {@link SecurityManager} are not considered. + * + *

This result type is mainly intended to help enforce the access checks of + * the Java Platform Module System. It allows detecting illegal access, even if + * the used Java version would only log a warning, or is configured to open + * packages for reflection using command line arguments. + * + * @see AccessibleObject#canAccess(Object) + */ + BLOCK_INACCESSIBLE, + /** + * Blocks all reflection access for the class. Other means for serializing + * and deserializing the class, such as a {@link TypeAdapter}, have to + * be used. + */ + BLOCK_ALL + } + + /** + * Blocks all reflection access to members of standard Java classes which are + * not accessible by default. However, reflection access is still allowed for + * classes for which all fields are accessible and which have an accessible + * no-args constructor (or for which an {@link InstanceCreator} has been registered). + * + *

If this filter encounters a class other than a standard Java class it + * returns {@link FilterResult#INDECISIVE}. + * + *

This filter is mainly intended to help enforcing the access checks of + * Java Platform Module System. It allows detecting illegal access, even if + * the used Java version would only log a warning, or is configured to open + * packages for reflection. However, this filter only works for Java 9 and + * higher, when using an older Java version its functionality will be + * limited. + * + *

Note that this filter might not cover all standard Java classes. Currently + * only classes in a {@code java.*} or {@code javax.*} package are considered. The + * set of detected classes might be expanded in the future without prior notice. + * + * @see FilterResult#BLOCK_INACCESSIBLE + */ + ReflectionAccessFilter BLOCK_INACCESSIBLE_JAVA = new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return ReflectionAccessFilterHelper.isJavaType(rawClass) + ? FilterResult.BLOCK_INACCESSIBLE + : FilterResult.INDECISIVE; + } + }; + + /** + * Blocks all reflection access to members of standard Java classes. + * + *

If this filter encounters a class other than a standard Java class it + * returns {@link FilterResult#INDECISIVE}. + * + *

This filter is mainly intended to prevent depending on implementation + * details of the Java platform and to help applications prepare for upgrading + * to the Java Platform Module System. + * + *

Note that this filter might not cover all standard Java classes. Currently + * only classes in a {@code java.*} or {@code javax.*} package are considered. The + * set of detected classes might be expanded in the future without prior notice. + * + * @see #BLOCK_INACCESSIBLE_JAVA + * @see FilterResult#BLOCK_ALL + */ + ReflectionAccessFilter BLOCK_ALL_JAVA = new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return ReflectionAccessFilterHelper.isJavaType(rawClass) + ? FilterResult.BLOCK_ALL + : FilterResult.INDECISIVE; + } + }; + + /** + * Blocks all reflection access to members of standard Android classes. + * + *

If this filter encounters a class other than a standard Android class it + * returns {@link FilterResult#INDECISIVE}. + * + *

This filter is mainly intended to prevent depending on implementation + * details of the Android platform. + * + *

Note that this filter might not cover all standard Android classes. Currently + * only classes in an {@code android.*} or {@code androidx.*} package, and standard + * Java classes in a {@code java.*} or {@code javax.*} package are considered. The + * set of detected classes might be expanded in the future without prior notice. + * + * @see FilterResult#BLOCK_ALL + */ + ReflectionAccessFilter BLOCK_ALL_ANDROID = new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return ReflectionAccessFilterHelper.isAndroidType(rawClass) + ? FilterResult.BLOCK_ALL + : FilterResult.INDECISIVE; + } + }; + + /** + * Blocks all reflection access to members of classes belonging to programming + * language platforms, such as Java, Android, Kotlin or Scala. + * + *

If this filter encounters a class other than a standard platform class it + * returns {@link FilterResult#INDECISIVE}. + * + *

This filter is mainly intended to prevent depending on implementation + * details of the platform classes. + * + *

Note that this filter might not cover all platform classes. Currently it + * combines the filters {@link #BLOCK_ALL_JAVA} and {@link #BLOCK_ALL_ANDROID}, + * and checks for other language-specific platform classes like {@code kotlin.*}. + * The set of detected classes might be expanded in the future without prior notice. + * + * @see FilterResult#BLOCK_ALL + */ + ReflectionAccessFilter BLOCK_ALL_PLATFORM = new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return ReflectionAccessFilterHelper.isAnyPlatformType(rawClass) + ? FilterResult.BLOCK_ALL + : FilterResult.INDECISIVE; + } + }; + + /** + * Checks if reflection access should be allowed for a class. + * + * @param rawClass + * Class to check + * @return + * Result indicating whether reflection access is allowed + */ + FilterResult check(Class rawClass); +} diff --git a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java index 489e37db07..0dfd134be8 100644 --- a/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java +++ b/gson/src/main/java/com/google/gson/internal/ConstructorConstructor.java @@ -16,6 +16,12 @@ package com.google.gson.internal; +import com.google.gson.InstanceCreator; +import com.google.gson.JsonIOException; +import com.google.gson.ReflectionAccessFilter; +import com.google.gson.ReflectionAccessFilter.FilterResult; +import com.google.gson.internal.reflect.ReflectionHelper; +import com.google.gson.reflect.TypeToken; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; @@ -28,6 +34,7 @@ import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; @@ -40,21 +47,18 @@ import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; -import com.google.gson.InstanceCreator; -import com.google.gson.JsonIOException; -import com.google.gson.internal.reflect.ReflectionHelper; -import com.google.gson.reflect.TypeToken; - /** * Returns a function that can construct an instance of a requested type. */ public final class ConstructorConstructor { private final Map> instanceCreators; private final boolean useJdkUnsafe; + private final List reflectionFilters; - public ConstructorConstructor(Map> instanceCreators, boolean useJdkUnsafe) { + public ConstructorConstructor(Map> instanceCreators, boolean useJdkUnsafe, List reflectionFilters) { this.instanceCreators = instanceCreators; this.useJdkUnsafe = useJdkUnsafe; + this.reflectionFilters = reflectionFilters; } public ObjectConstructor get(TypeToken typeToken) { @@ -85,7 +89,16 @@ public ObjectConstructor get(TypeToken typeToken) { }; } - ObjectConstructor defaultConstructor = newDefaultConstructor(rawType); + // First consider special constructors before checking for no-args constructors + // below to avoid matching internal no-args constructors which might be added in + // future JDK versions + ObjectConstructor specialConstructor = newSpecialCollectionConstructor(type, rawType); + if (specialConstructor != null) { + return specialConstructor; + } + + FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType); + ObjectConstructor defaultConstructor = newDefaultConstructor(rawType, filterResult); if (defaultConstructor != null) { return defaultConstructor; } @@ -95,11 +108,81 @@ public ObjectConstructor get(TypeToken typeToken) { return defaultImplementation; } - // finally try unsafe - return newUnsafeAllocator(rawType); + // Check whether type is instantiable; otherwise ReflectionAccessFilter recommendation + // of adjusting filter suggested below is irrelevant since it would not solve the problem + final String exceptionMessage = UnsafeAllocator.checkInstantiable(rawType); + if (exceptionMessage != null) { + return new ObjectConstructor() { + @Override public T construct() { + throw new JsonIOException(exceptionMessage); + } + }; + } + + // Consider usage of Unsafe as reflection, so don't use if BLOCK_ALL + // Additionally, since it is not calling any constructor at all, don't use if BLOCK_INACCESSIBLE + if (filterResult == FilterResult.ALLOW) { + // finally try unsafe + return newUnsafeAllocator(rawType); + } else { + final String message = "Unable to create instance of " + rawType + "; ReflectionAccessFilter " + + "does not permit using reflection or Unsafe. Register an InstanceCreator or a TypeAdapter " + + "for this type or adjust the access filter to allow using reflection."; + return new ObjectConstructor() { + @Override public T construct() { + throw new JsonIOException(message); + } + }; + } } - private ObjectConstructor newDefaultConstructor(Class rawType) { + /** + * Creates constructors for special JDK collection types which do not have a public no-args constructor. + */ + private static ObjectConstructor newSpecialCollectionConstructor(final Type type, Class rawType) { + if (EnumSet.class.isAssignableFrom(rawType)) { + return new ObjectConstructor() { + @Override public T construct() { + if (type instanceof ParameterizedType) { + Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + if (elementType instanceof Class) { + @SuppressWarnings({"unchecked", "rawtypes"}) + T set = (T) EnumSet.noneOf((Class)elementType); + return set; + } else { + throw new JsonIOException("Invalid EnumSet type: " + type.toString()); + } + } else { + throw new JsonIOException("Invalid EnumSet type: " + type.toString()); + } + } + }; + } + // Only support creation of EnumMap, but not of custom subtypes; for them type parameters + // and constructor parameter might have completely different meaning + else if (rawType == EnumMap.class) { + return new ObjectConstructor() { + @Override public T construct() { + if (type instanceof ParameterizedType) { + Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + if (elementType instanceof Class) { + @SuppressWarnings({"unchecked", "rawtypes"}) + T map = (T) new EnumMap((Class) elementType); + return map; + } else { + throw new JsonIOException("Invalid EnumMap type: " + type.toString()); + } + } else { + throw new JsonIOException("Invalid EnumMap type: " + type.toString()); + } + } + }; + } + + return null; + } + + private static ObjectConstructor newDefaultConstructor(Class rawType, FilterResult filterResult) { // Cannot invoke constructor of abstract class if (Modifier.isAbstract(rawType.getModifiers())) { return null; @@ -112,27 +195,47 @@ private ObjectConstructor newDefaultConstructor(Class rawType) return null; } - final String exceptionMessage = ReflectionHelper.tryMakeAccessible(constructor); - if (exceptionMessage != null) { - /* - * Create ObjectConstructor which throws exception. - * This keeps backward compatibility (compared to returning `null` which - * would then choose another way of creating object). - * And it supports types which are only serialized but not deserialized - * (compared to directly throwing exception here), e.g. when runtime type - * of object is inaccessible, but compile-time type is accessible. - */ + boolean canAccess = filterResult == FilterResult.ALLOW || (ReflectionAccessFilterHelper.canAccess(constructor, null) + // Be a bit more lenient here for BLOCK_ALL; if constructor is accessible and public then allow calling it + && (filterResult != FilterResult.BLOCK_ALL || Modifier.isPublic(constructor.getModifiers()))); + + if (!canAccess) { + final String message = "Unable to invoke no-args constructor of " + rawType + "; " + + "constructor is not accessible and ReflectionAccessFilter does not permit making " + + "it accessible. Register an InstanceCreator or a TypeAdapter for this type, change " + + "the visibility of the constructor or adjust the access filter."; return new ObjectConstructor() { - @Override - public T construct() { - // New exception is created every time to avoid keeping reference - // to exception with potentially long stack trace, causing a - // memory leak - throw new JsonIOException(exceptionMessage); + @Override public T construct() { + throw new JsonIOException(message); } }; } + // Only try to make accessible if allowed; in all other cases checks above should + // have verified that constructor is accessible + if (filterResult == FilterResult.ALLOW) { + final String exceptionMessage = ReflectionHelper.tryMakeAccessible(constructor); + if (exceptionMessage != null) { + /* + * Create ObjectConstructor which throws exception. + * This keeps backward compatibility (compared to returning `null` which + * would then choose another way of creating object). + * And it supports types which are only serialized but not deserialized + * (compared to directly throwing exception here), e.g. when runtime type + * of object is inaccessible, but compile-time type is accessible. + */ + return new ObjectConstructor() { + @Override + public T construct() { + // New exception is created every time to avoid keeping reference + // to exception with potentially long stack trace, causing a + // memory leak + throw new JsonIOException(exceptionMessage); + } + }; + } + } + return new ObjectConstructor() { @Override public T construct() { try { @@ -148,7 +251,7 @@ public T construct() { throw new RuntimeException("Failed to invoke " + constructor + " with no args", e.getTargetException()); } catch (IllegalAccessException e) { - throw new AssertionError(e); + throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } } }; @@ -159,8 +262,17 @@ public T construct() { * subtypes. */ @SuppressWarnings("unchecked") // use runtime checks to guarantee that 'T' is what it is - private ObjectConstructor newDefaultImplementationConstructor( + private static ObjectConstructor newDefaultImplementationConstructor( final Type type, Class rawType) { + + /* + * IMPORTANT: Must only create instances for classes with public no-args constructor. + * For classes with special constructors / factory methods (e.g. EnumSet) + * `newSpecialCollectionConstructor` defined above must be used, to avoid no-args + * constructor check (which is called before this method) detecting internal no-args + * constructors which might be added in a future JDK version + */ + if (Collection.class.isAssignableFrom(rawType)) { if (SortedSet.class.isAssignableFrom(rawType)) { return new ObjectConstructor() { @@ -168,22 +280,6 @@ private ObjectConstructor newDefaultImplementationConstructor( return (T) new TreeSet(); } }; - } else if (EnumSet.class.isAssignableFrom(rawType)) { - return new ObjectConstructor() { - @SuppressWarnings("rawtypes") - @Override public T construct() { - if (type instanceof ParameterizedType) { - Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; - if (elementType instanceof Class) { - return (T) EnumSet.noneOf((Class)elementType); - } else { - throw new JsonIOException("Invalid EnumSet type: " + type.toString()); - } - } else { - throw new JsonIOException("Invalid EnumSet type: " + type.toString()); - } - } - }; } else if (Set.class.isAssignableFrom(rawType)) { return new ObjectConstructor() { @Override public T construct() { @@ -206,26 +302,7 @@ private ObjectConstructor newDefaultImplementationConstructor( } if (Map.class.isAssignableFrom(rawType)) { - // Only support creation of EnumMap, but not of custom subtypes; for them type parameters - // and constructor parameter might have completely different meaning - if (rawType == EnumMap.class) { - return new ObjectConstructor() { - @Override public T construct() { - if (type instanceof ParameterizedType) { - Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; - if (elementType instanceof Class) { - @SuppressWarnings("rawtypes") - T map = (T) new EnumMap((Class) elementType); - return map; - } else { - throw new JsonIOException("Invalid EnumMap type: " + type.toString()); - } - } else { - throw new JsonIOException("Invalid EnumMap type: " + type.toString()); - } - } - }; - } else if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) { + if (ConcurrentNavigableMap.class.isAssignableFrom(rawType)) { return new ObjectConstructor() { @Override public T construct() { return (T) new ConcurrentSkipListMap(); diff --git a/gson/src/main/java/com/google/gson/internal/ReflectionAccessFilterHelper.java b/gson/src/main/java/com/google/gson/internal/ReflectionAccessFilterHelper.java new file mode 100644 index 0000000000..a07b2c73d5 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/ReflectionAccessFilterHelper.java @@ -0,0 +1,101 @@ +package com.google.gson.internal; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.util.List; + +import com.google.gson.ReflectionAccessFilter; +import com.google.gson.ReflectionAccessFilter.FilterResult; + +/** + * Internal helper class for {@link ReflectionAccessFilter}. + */ +public class ReflectionAccessFilterHelper { + private ReflectionAccessFilterHelper() { } + + // Platform type detection is based on Moshi's Util.isPlatformType(Class) + // See https://github.com/square/moshi/blob/3c108919ee1cce88a433ffda04eeeddc0341eae7/moshi/src/main/java/com/squareup/moshi/internal/Util.java#L141 + + public static boolean isJavaType(Class c) { + return isJavaType(c.getName()); + } + + private static boolean isJavaType(String className) { + return className.startsWith("java.") || className.startsWith("javax."); + } + + public static boolean isAndroidType(Class c) { + return isAndroidType(c.getName()); + } + + private static boolean isAndroidType(String className) { + return className.startsWith("android.") + || className.startsWith("androidx.") + || isJavaType(className); + } + + public static boolean isAnyPlatformType(Class c) { + String className = c.getName(); + return isAndroidType(className) // Covers Android and Java + || className.startsWith("kotlin.") + || className.startsWith("kotlinx.") + || className.startsWith("scala."); + } + + /** + * Gets the result of applying all filters until the first one returns a result + * other than {@link FilterResult#INDECISIVE}, or {@link FilterResult#ALLOW} if + * the list of filters is empty or all returned {@code INDECISIVE}. + */ + public static FilterResult getFilterResult(List reflectionFilters, Class c) { + for (ReflectionAccessFilter filter : reflectionFilters) { + FilterResult result = filter.check(c); + if (result != FilterResult.INDECISIVE) { + return result; + } + } + return FilterResult.ALLOW; + } + + /** + * See {@link AccessibleObject#canAccess(Object)} (Java >= 9) + */ + public static boolean canAccess(AccessibleObject accessibleObject, Object object) { + return AccessChecker.INSTANCE.canAccess(accessibleObject, object); + } + + private static abstract class AccessChecker { + public static final AccessChecker INSTANCE; + static { + AccessChecker accessChecker = null; + // TODO: Ideally should use Multi-Release JAR for this version specific code + if (JavaVersion.isJava9OrLater()) { + try { + final Method canAccessMethod = AccessibleObject.class.getDeclaredMethod("canAccess", Object.class); + accessChecker = new AccessChecker() { + @Override public boolean canAccess(AccessibleObject accessibleObject, Object object) { + try { + return (Boolean) canAccessMethod.invoke(accessibleObject, object); + } catch (Exception e) { + throw new RuntimeException("Failed invoking canAccess", e); + } + } + }; + } catch (NoSuchMethodException ignored) { + } + } + + if (accessChecker == null) { + accessChecker = new AccessChecker() { + @Override public boolean canAccess(AccessibleObject accessibleObject, Object object) { + // Cannot determine whether object can be accessed, so assume it can be accessed + return true; + } + }; + } + INSTANCE = accessChecker; + } + + public abstract boolean canAccess(AccessibleObject accessibleObject, Object object); + } +} diff --git a/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java index 7060a22eb6..429bac6be7 100644 --- a/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java +++ b/gson/src/main/java/com/google/gson/internal/UnsafeAllocator.java @@ -31,6 +31,37 @@ public abstract class UnsafeAllocator { public abstract T newInstance(Class c) throws Exception; + /** + * Check if the class can be instantiated by Unsafe allocator. If the instance has interface or abstract modifiers + * return an exception message. + * @param c instance of the class to be checked + * @return if instantiable {@code null}, else a non-{@code null} exception message + */ + static String checkInstantiable(Class c) { + int modifiers = c.getModifiers(); + if (Modifier.isInterface(modifiers)) { + return "Interfaces can't be instantiated! Register an InstanceCreator " + + "or a TypeAdapter for this type. Interface name: " + c.getName(); + } + if (Modifier.isAbstract(modifiers)) { + return "Abstract classes can't be instantiated! Register an InstanceCreator " + + "or a TypeAdapter for this type. Class name: " + c.getName(); + } + return null; + } + + /** + * Asserts that the class is instantiable. This check should have already occurred + * in {@link ConstructorConstructor}; this check here acts as safeguard since trying + * to use Unsafe for non-instantiable classes might crash the JVM on some devices. + */ + private static void assertInstantiable(Class c) { + String exceptionMessage = checkInstantiable(c); + if (exceptionMessage != null) { + throw new AssertionError("UnsafeAllocator is used for non-instantiable type: " + exceptionMessage); + } + } + public static UnsafeAllocator create() { // try JVM // public class Unsafe { @@ -106,19 +137,4 @@ public T newInstance(Class c) { } }; } - - /** - * Check if the class can be instantiated by unsafe allocator. If the instance has interface or abstract modifiers - * throw an {@link java.lang.UnsupportedOperationException} - * @param c instance of the class to be checked - */ - static void assertInstantiable(Class c) { - int modifiers = c.getModifiers(); - if (Modifier.isInterface(modifiers)) { - throw new UnsupportedOperationException("Interface can't be instantiated! Interface name: " + c.getName()); - } - if (Modifier.isAbstract(modifiers)) { - throw new UnsupportedOperationException("Abstract class can't be instantiated! Class name: " + c.getName()); - } - } } diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 21c049e23c..68b0a4eb5b 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -18,7 +18,10 @@ import com.google.gson.FieldNamingStrategy; import com.google.gson.Gson; +import com.google.gson.JsonIOException; import com.google.gson.JsonSyntaxException; +import com.google.gson.ReflectionAccessFilter; +import com.google.gson.ReflectionAccessFilter.FilterResult; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.JsonAdapter; @@ -28,6 +31,7 @@ import com.google.gson.internal.Excluder; import com.google.gson.internal.ObjectConstructor; import com.google.gson.internal.Primitives; +import com.google.gson.internal.ReflectionAccessFilterHelper; import com.google.gson.internal.reflect.ReflectionHelper; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -35,6 +39,7 @@ import com.google.gson.stream.JsonWriter; import java.io.IOException; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; @@ -50,14 +55,17 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory { private final FieldNamingStrategy fieldNamingPolicy; private final Excluder excluder; private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory; + private final List reflectionFilters; public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor, FieldNamingStrategy fieldNamingPolicy, Excluder excluder, - JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory) { + JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory, + List reflectionFilters) { this.constructorConstructor = constructorConstructor; this.fieldNamingPolicy = fieldNamingPolicy; this.excluder = excluder; this.jsonAdapterFactory = jsonAdapterFactory; + this.reflectionFilters = reflectionFilters; } public boolean excludeField(Field f, boolean serialize) { @@ -97,13 +105,30 @@ private List getFieldNames(Field f) { return null; // it's a primitive! } + FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw); + if (filterResult == FilterResult.BLOCK_ALL) { + throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " + + raw + ". Register a TypeAdapter for this type or adjust the access filter."); + } + boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE; + ObjectConstructor constructor = constructorConstructor.get(type); - return new Adapter(constructor, getBoundFields(gson, type, raw)); + return new Adapter(constructor, getBoundFields(gson, type, raw, blockInaccessible)); + } + + private static void checkAccessible(Object object, Field field) { + if (!ReflectionAccessFilterHelper.canAccess(field, Modifier.isStatic(field.getModifiers()) ? null : object)) { + throw new JsonIOException("Field '" + field.getDeclaringClass().getName() + "#" + + field.getName() + "' is not accessible and ReflectionAccessFilter does not " + + "permit making it accessible. Register a TypeAdapter for the declaring type " + + "or adjust the access filter."); + } } private ReflectiveTypeAdapterFactory.BoundField createBoundField( final Gson context, final Field field, final String name, - final TypeToken fieldType, boolean serialize, boolean deserialize) { + final TypeToken fieldType, boolean serialize, boolean deserialize, + final boolean blockInaccessible) { final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType()); // special casing primitives here saves ~5% on Android... JsonAdapter annotation = field.getAnnotation(JsonAdapter.class); @@ -120,7 +145,17 @@ private ReflectiveTypeAdapterFactory.BoundField createBoundField( @SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree @Override void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException { + if (!serialized) return; + if (blockInaccessible) { + checkAccessible(value, field); + } + Object fieldValue = field.get(value); + if (fieldValue == value) { + // avoid direct recursion + return; + } + writer.name(name); TypeAdapter t = jsonAdapterPresent ? typeAdapter : new TypeAdapterRuntimeTypeWrapper(context, typeAdapter, fieldType.getType()); t.write(writer, fieldValue); @@ -129,33 +164,48 @@ private ReflectiveTypeAdapterFactory.BoundField createBoundField( throws IOException, IllegalAccessException { Object fieldValue = typeAdapter.read(reader); if (fieldValue != null || !isPrimitive) { + if (blockInaccessible) { + checkAccessible(value, field); + } field.set(value, fieldValue); } } - @Override public boolean writeField(Object value) throws IOException, IllegalAccessException { - if (!serialized) return false; - Object fieldValue = field.get(value); - return fieldValue != value; // avoid recursion for example for Throwable.cause - } }; } - private Map getBoundFields(Gson context, TypeToken type, Class raw) { + private Map getBoundFields(Gson context, TypeToken type, Class raw, boolean blockInaccessible) { Map result = new LinkedHashMap(); if (raw.isInterface()) { return result; } Type declaredType = type.getType(); + Class originalRaw = raw; while (raw != Object.class) { Field[] fields = raw.getDeclaredFields(); + + // For inherited fields, check if access to their declaring class is allowed + if (raw != originalRaw && fields.length > 0) { + FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw); + if (filterResult == FilterResult.BLOCK_ALL) { + throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " + + raw + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type " + + "or adjust the access filter."); + } + blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE; + } + for (Field field : fields) { boolean serialize = excludeField(field, true); boolean deserialize = excludeField(field, false); if (!serialize && !deserialize) { continue; } - ReflectionHelper.makeAccessible(field); + + // If blockInaccessible, skip and perform access check later + if (!blockInaccessible) { + ReflectionHelper.makeAccessible(field); + } Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType()); List fieldNames = getFieldNames(field); BoundField previous = null; @@ -163,7 +213,7 @@ private Map getBoundFields(Gson context, TypeToken type, String name = fieldNames.get(i); if (i != 0) serialize = false; // only serialize the default name BoundField boundField = createBoundField(context, field, name, - TypeToken.get(fieldType), serialize, deserialize); + TypeToken.get(fieldType), serialize, deserialize, blockInaccessible); BoundField replaced = result.put(name, boundField); if (previous == null) previous = replaced; } @@ -188,7 +238,6 @@ protected BoundField(String name, boolean serialized, boolean deserialized) { this.serialized = serialized; this.deserialized = deserialized; } - abstract boolean writeField(Object value) throws IOException, IllegalAccessException; abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException; abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException; } @@ -224,7 +273,7 @@ public static final class Adapter extends TypeAdapter { } catch (IllegalStateException e) { throw new JsonSyntaxException(e); } catch (IllegalAccessException e) { - throw new AssertionError(e); + throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } in.endObject(); return instance; @@ -239,13 +288,10 @@ public static final class Adapter extends TypeAdapter { out.beginObject(); try { for (BoundField boundField : boundFields.values()) { - if (boundField.writeField(value)) { - out.name(boundField.name); - boundField.write(out, value); - } + boundField.write(out, value); } } catch (IllegalAccessException e) { - throw new AssertionError(e); + throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e); } out.endObject(); } diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index a74de3025b..97230ff6f5 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -1,6 +1,7 @@ package com.google.gson.internal.reflect; import com.google.gson.JsonIOException; +import com.google.gson.internal.GsonBuildConfig; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -63,4 +64,11 @@ public static String tryMakeAccessible(Constructor constructor) { + exception.getMessage(); } } + + public static RuntimeException createExceptionForUnexpectedIllegalAccess(IllegalAccessException exception) { + throw new RuntimeException("Unexpected IllegalAccessException occurred (Gson " + GsonBuildConfig.VERSION + "). " + + "Certain ReflectionAccessFilter features require Java >= 9 to work correctly. If you are not using " + + "ReflectionAccessFilter, report this to the Gson maintainers.", + exception); + } } diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index 186ceec9bd..abb0de2113 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -27,6 +27,7 @@ import java.lang.reflect.Type; import java.text.DateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import junit.framework.TestCase; @@ -56,7 +57,8 @@ public void testOverridesDefaultExcluder() { true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), - CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY); + CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY, + Collections.emptyList()); assertEquals(CUSTOM_EXCLUDER, gson.excluder); assertEquals(CUSTOM_FIELD_NAMING_STRATEGY, gson.fieldNamingStrategy()); @@ -70,7 +72,8 @@ public void testClonedTypeAdapterFactoryListsAreIndependent() { true, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), - CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY); + CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY, + Collections.emptyList()); Gson clone = original.newBuilder() .registerTypeAdapter(Object.class, new TestTypeAdapter()) diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java new file mode 100644 index 0000000000..775baf9f90 --- /dev/null +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessFilterTest.java @@ -0,0 +1,426 @@ +package com.google.gson.functional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.JsonElement; +import com.google.gson.JsonIOException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.ReflectionAccessFilter; +import com.google.gson.ReflectionAccessFilter.FilterResult; +import com.google.gson.TypeAdapter; +import com.google.gson.internal.ConstructorConstructor; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.awt.Point; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.lang.reflect.Type; +import java.util.LinkedList; +import java.util.List; +import org.junit.Test; + +public class ReflectionAccessFilterTest { + // Reader has protected `lock` field which cannot be accessed + private static class ClassExtendingJdkClass extends Reader { + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + return 0; + } + + @Override + public void close() throws IOException { + } + } + + @Test + public void testBlockInaccessibleJava() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA) + .create(); + + // Serialization should fail for classes with non-public fields + try { + gson.toJson(new File("a")); + fail("Expected exception; test needs to be run with Java >= 9"); + } catch (JsonIOException expected) { + // Note: This test is rather brittle and depends on the JDK implementation + assertEquals( + "Field 'java.io.File#path' is not accessible and ReflectionAccessFilter does not permit " + + "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.", + expected.getMessage() + ); + } + + // But serialization should succeed for classes with only public fields + String json = gson.toJson(new Point(1, 2)); + assertEquals("{\"x\":1,\"y\":2}", json); + } + + @Test + public void testBlockInaccessibleJavaExtendingJdkClass() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA) + .create(); + + try { + gson.toJson(new ClassExtendingJdkClass()); + fail("Expected exception; test needs to be run with Java >= 9"); + } catch (JsonIOException expected) { + assertEquals( + "Field 'java.io.Reader#lock' is not accessible and ReflectionAccessFilter does not permit " + + "making it accessible. Register a TypeAdapter for the declaring type or adjust the access filter.", + expected.getMessage() + ); + } + } + + @Test + public void testBlockAllJava() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA) + .create(); + + // Serialization should fail for any Java class + try { + gson.toJson(Thread.currentThread()); + fail(); + } catch (JsonIOException expected) { + assertEquals( + "ReflectionAccessFilter does not permit using reflection for class java.lang.Thread. " + + "Register a TypeAdapter for this type or adjust the access filter.", + expected.getMessage() + ); + } + } + + @Test + public void testBlockAllJavaExtendingJdkClass() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(ReflectionAccessFilter.BLOCK_ALL_JAVA) + .create(); + + try { + gson.toJson(new ClassExtendingJdkClass()); + fail(); + } catch (JsonIOException expected) { + assertEquals( + "ReflectionAccessFilter does not permit using reflection for class java.io.Reader " + + "(supertype of class com.google.gson.functional.ReflectionAccessFilterTest$ClassExtendingJdkClass). " + + "Register a TypeAdapter for this type or adjust the access filter.", + expected.getMessage() + ); + } + } + + private static class ClassWithStaticField { + @SuppressWarnings("unused") + private static int i = 1; + } + + @Test + public void testBlockInaccessibleStaticField() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return FilterResult.BLOCK_INACCESSIBLE; + } + }) + // Include static fields + .excludeFieldsWithModifiers(0) + .create(); + + try { + gson.toJson(new ClassWithStaticField()); + fail("Expected exception; test needs to be run with Java >= 9"); + } catch (JsonIOException expected) { + assertEquals( + "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithStaticField#i' " + + "is not accessible and ReflectionAccessFilter does not permit making it accessible. " + + "Register a TypeAdapter for the declaring type or adjust the access filter.", + expected.getMessage() + ); + } + } + + private static class SuperTestClass { + } + private static class SubTestClass extends SuperTestClass { + @SuppressWarnings("unused") + public int i = 1; + } + private static class OtherClass { + @SuppressWarnings("unused") + public int i = 2; + } + + @Test + public void testDelegation() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + // INDECISIVE in last filter should act like ALLOW + return SuperTestClass.class.isAssignableFrom(rawClass) ? FilterResult.BLOCK_ALL : FilterResult.INDECISIVE; + } + }) + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + // INDECISIVE should delegate to previous filter + return rawClass == SubTestClass.class ? FilterResult.ALLOW : FilterResult.INDECISIVE; + } + }) + .create(); + + // Filter disallows SuperTestClass + try { + gson.toJson(new SuperTestClass()); + fail(); + } catch (JsonIOException expected) { + assertEquals( + "ReflectionAccessFilter does not permit using reflection for class " + + "com.google.gson.functional.ReflectionAccessFilterTest$SuperTestClass. " + + "Register a TypeAdapter for this type or adjust the access filter.", + expected.getMessage() + ); + } + + // But registration order is reversed, so filter for SubTestClass allows reflection + String json = gson.toJson(new SubTestClass()); + assertEquals("{\"i\":1}", json); + + // And unrelated class should not be affected + json = gson.toJson(new OtherClass()); + assertEquals("{\"i\":2}", json); + } + + private static class ClassWithPrivateField { + @SuppressWarnings("unused") + private int i = 1; + } + private static class ExtendingClassWithPrivateField extends ClassWithPrivateField { + } + + @Test + public void testAllowForSupertype() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return FilterResult.BLOCK_INACCESSIBLE; + } + }) + .create(); + + // First make sure test is implemented correctly and access is blocked + try { + gson.toJson(new ExtendingClassWithPrivateField()); + fail("Expected exception; test needs to be run with Java >= 9"); + } catch (JsonIOException expected) { + assertEquals( + "Field 'com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateField#i' " + + "is not accessible and ReflectionAccessFilter does not permit making it accessible. " + + "Register a TypeAdapter for the declaring type or adjust the access filter.", + expected.getMessage() + ); + } + + gson = gson.newBuilder() + // Allow reflective access for supertype + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return rawClass == ClassWithPrivateField.class ? FilterResult.ALLOW : FilterResult.INDECISIVE; + } + }) + .create(); + + // Inherited (inaccessible) private field should have been made accessible + String json = gson.toJson(new ExtendingClassWithPrivateField()); + assertEquals("{\"i\":1}", json); + } + + private static class ClassWithPrivateNoArgsConstructor { + private ClassWithPrivateNoArgsConstructor() { + } + } + + @Test + public void testInaccessibleNoArgsConstructor() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return FilterResult.BLOCK_INACCESSIBLE; + } + }) + .create(); + + try { + gson.fromJson("{}", ClassWithPrivateNoArgsConstructor.class); + fail("Expected exception; test needs to be run with Java >= 9"); + } catch (JsonIOException expected) { + assertEquals( + "Unable to invoke no-args constructor of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithPrivateNoArgsConstructor; " + + "constructor is not accessible and ReflectionAccessFilter does not permit making it accessible. Register an " + + "InstanceCreator or a TypeAdapter for this type, change the visibility of the constructor or adjust the access filter.", + expected.getMessage() + ); + } + } + + private static class ClassWithoutNoArgsConstructor { + public String s; + + public ClassWithoutNoArgsConstructor(String s) { + this.s = s; + } + } + + @Test + public void testClassWithoutNoArgsConstructor() { + GsonBuilder gsonBuilder = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + // Even BLOCK_INACCESSIBLE should prevent usage of Unsafe for object creation + return FilterResult.BLOCK_INACCESSIBLE; + } + }); + Gson gson = gsonBuilder.create(); + + try { + gson.fromJson("{}", ClassWithoutNoArgsConstructor.class); + fail(); + } catch (JsonIOException expected) { + assertEquals( + "Unable to create instance of class com.google.gson.functional.ReflectionAccessFilterTest$ClassWithoutNoArgsConstructor; " + + "ReflectionAccessFilter does not permit using reflection or Unsafe. Register an InstanceCreator " + + "or a TypeAdapter for this type or adjust the access filter to allow using reflection.", + expected.getMessage() + ); + } + + // But should not fail when custom TypeAdapter is specified + gson = gson.newBuilder() + .registerTypeAdapter(ClassWithoutNoArgsConstructor.class, new TypeAdapter() { + @Override public ClassWithoutNoArgsConstructor read(JsonReader in) throws IOException { + in.skipValue(); + return new ClassWithoutNoArgsConstructor("TypeAdapter"); + } + @Override public void write(JsonWriter out, ClassWithoutNoArgsConstructor value) throws IOException { + throw new AssertionError("Not needed for test"); + }; + }) + .create(); + ClassWithoutNoArgsConstructor deserialized = gson.fromJson("{}", ClassWithoutNoArgsConstructor.class); + assertEquals("TypeAdapter", deserialized.s); + + // But should not fail when custom InstanceCreator is specified + gson = gsonBuilder + .registerTypeAdapter(ClassWithoutNoArgsConstructor.class, new InstanceCreator() { + @Override public ClassWithoutNoArgsConstructor createInstance(Type type) { + return new ClassWithoutNoArgsConstructor("InstanceCreator"); + } + }) + .create(); + deserialized = gson.fromJson("{}", ClassWithoutNoArgsConstructor.class); + assertEquals("InstanceCreator", deserialized.s); + } + + /** + * When using {@link FilterResult#BLOCK_ALL}, registering only a {@link JsonSerializer} + * but not performing any deserialization should not throw any exception. + */ + @Test + public void testBlockAllPartial() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return FilterResult.BLOCK_ALL; + } + }) + .registerTypeAdapter(OtherClass.class, new JsonSerializer() { + @Override public JsonElement serialize(OtherClass src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(123); + } + }) + .create(); + + String json = gson.toJson(new OtherClass()); + assertEquals("123", json); + + // But deserialization should fail + try { + gson.fromJson("{}", OtherClass.class); + fail(); + } catch (JsonIOException expected) { + assertEquals( + "ReflectionAccessFilter does not permit using reflection for class com.google.gson.functional.ReflectionAccessFilterTest$OtherClass. " + + "Register a TypeAdapter for this type or adjust the access filter.", + expected.getMessage() + ); + } + } + + /** + * Should not fail when deserializing collection interface + * (Even though this goes through {@link ConstructorConstructor} as well) + */ + @Test + public void testBlockAllCollectionInterface() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return FilterResult.BLOCK_ALL; + } + }) + .create(); + List deserialized = gson.fromJson("[1.0]", List.class); + assertEquals(1.0, deserialized.get(0)); + } + + /** + * Should not fail when deserializing specific collection implementation + * (Even though this goes through {@link ConstructorConstructor} as well) + */ + @Test + public void testBlockAllCollectionImplementation() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return FilterResult.BLOCK_ALL; + } + }) + .create(); + List deserialized = gson.fromJson("[1.0]", LinkedList.class); + assertEquals(1.0, deserialized.get(0)); + } + + /** + * When trying to deserialize interface an exception for that should + * be thrown, even if {@link FilterResult#BLOCK_INACCESSIBLE} is used + */ + @Test + public void testBlockInaccessibleInterface() { + Gson gson = new GsonBuilder() + .addReflectionAccessFilter(new ReflectionAccessFilter() { + @Override public FilterResult check(Class rawClass) { + return FilterResult.BLOCK_INACCESSIBLE; + } + }) + .create(); + + try { + gson.fromJson("{}", Runnable.class); + fail(); + } catch (JsonIOException expected) { + assertEquals( + "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for " + + "this type. Interface name: java.lang.Runnable", + expected.getMessage() + ); + } + } +} diff --git a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java index e2940b88ee..ba3c339aa1 100644 --- a/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java +++ b/gson/src/test/java/com/google/gson/internal/ConstructorConstructorTest.java @@ -3,17 +3,18 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; +import com.google.gson.InstanceCreator; +import com.google.gson.ReflectionAccessFilter; +import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.Collections; -import java.util.Map; - import org.junit.Test; -import com.google.gson.InstanceCreator; -import com.google.gson.reflect.TypeToken; - public class ConstructorConstructorTest { - private static final Map> NO_INSTANCE_CREATORS = Collections.emptyMap(); + private ConstructorConstructor constructorConstructor = new ConstructorConstructor( + Collections.>emptyMap(), true, + Collections.emptyList() + ); private abstract static class AbstractClass { @SuppressWarnings("unused") @@ -27,15 +28,14 @@ private interface Interface { } */ @Test public void testGet_AbstractClassNoArgConstructor() { - ConstructorConstructor constructorFactory = new ConstructorConstructor(NO_INSTANCE_CREATORS, true); - ObjectConstructor constructor = constructorFactory.get(TypeToken.get(AbstractClass.class)); + ObjectConstructor constructor = constructorConstructor.get(TypeToken.get(AbstractClass.class)); try { constructor.construct(); fail("Expected exception"); } catch (RuntimeException exception) { assertEquals( - "Unable to create instance of class com.google.gson.internal.ConstructorConstructorTest$AbstractClass. " - + "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args constructor may fix this problem.", + "Abstract classes can't be instantiated! Register an InstanceCreator or a TypeAdapter for this " + + "type. Class name: com.google.gson.internal.ConstructorConstructorTest$AbstractClass", exception.getMessage() ); } @@ -43,15 +43,14 @@ public void testGet_AbstractClassNoArgConstructor() { @Test public void testGet_Interface() { - ConstructorConstructor constructorFactory = new ConstructorConstructor(NO_INSTANCE_CREATORS, true); - ObjectConstructor constructor = constructorFactory.get(TypeToken.get(Interface.class)); + ObjectConstructor constructor = constructorConstructor.get(TypeToken.get(Interface.class)); try { constructor.construct(); fail("Expected exception"); } catch (RuntimeException exception) { assertEquals( - "Unable to create instance of interface com.google.gson.internal.ConstructorConstructorTest$Interface. " - + "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args constructor may fix this problem.", + "Interfaces can't be instantiated! Register an InstanceCreator or a TypeAdapter for " + + "this type. Interface name: com.google.gson.internal.ConstructorConstructorTest$Interface", exception.getMessage() ); } diff --git a/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java b/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java index 9e1807899f..e3ce147e5b 100644 --- a/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java +++ b/gson/src/test/java/com/google/gson/internal/UnsafeAllocatorInstantiationTest.java @@ -33,42 +33,39 @@ public static class ConcreteClass { } /** - * Ensure that the {@link java.lang.UnsupportedOperationException} is thrown when trying + * Ensure that an {@link AssertionError} is thrown when trying * to instantiate an interface */ - public void testInterfaceInstantiation() { + public void testInterfaceInstantiation() throws Exception { UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); try { unsafeAllocator.newInstance(Interface.class); fail(); - } catch (Exception e) { - assertEquals(e.getClass(), UnsupportedOperationException.class); + } catch (AssertionError e) { + assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type")); } } /** - * Ensure that the {@link java.lang.UnsupportedOperationException} is thrown when trying + * Ensure that an {@link AssertionError} is thrown when trying * to instantiate an abstract class */ - public void testAbstractClassInstantiation() { + public void testAbstractClassInstantiation() throws Exception { UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); try { unsafeAllocator.newInstance(AbstractClass.class); fail(); - } catch (Exception e) { - assertEquals(e.getClass(), UnsupportedOperationException.class); + } catch (AssertionError e) { + assertTrue(e.getMessage().startsWith("UnsafeAllocator is used for non-instantiable type")); } } /** * Ensure that no exception is thrown when trying to instantiate a concrete class */ - public void testConcreteClassInstantiation() { + public void testConcreteClassInstantiation() throws Exception { UnsafeAllocator unsafeAllocator = UnsafeAllocator.create(); - try { - unsafeAllocator.newInstance(ConcreteClass.class); - } catch (Exception e) { - fail(); - } + ConcreteClass instance = unsafeAllocator.newInstance(ConcreteClass.class); + assertNotNull(instance); } }