| // Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| package com.android.tools.r8.shaking.forceproguardcompatibility.defaultctor; |
| |
| import static com.android.tools.r8.shaking.forceproguardcompatibility.defaultctor.ExternalizableDataClass.TYPE_1; |
| import static com.android.tools.r8.shaking.forceproguardcompatibility.defaultctor.ExternalizableDataClass.TYPE_2; |
| import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.assertEquals; |
| |
| import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatibilityTestBase; |
| import com.android.tools.r8.utils.AndroidApp; |
| import com.android.tools.r8.utils.codeinspector.ClassSubject; |
| import com.android.tools.r8.utils.codeinspector.CodeInspector; |
| import com.android.tools.r8.utils.codeinspector.MethodSubject; |
| import com.google.common.collect.ImmutableList; |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInput; |
| import java.io.DataOutput; |
| import java.io.Externalizable; |
| import java.io.IOException; |
| import java.io.InvalidClassException; |
| import java.io.InvalidObjectException; |
| import java.io.ObjectInput; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutput; |
| import java.io.ObjectOutputStream; |
| import java.io.ObjectStreamException; |
| import java.io.Serializable; |
| import java.util.Collection; |
| import java.util.List; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| final class ExternalizableDataClass implements Externalizable { |
| static final byte TYPE_1 = 1; |
| static final byte TYPE_2 = 2; |
| |
| private byte byteField; |
| private Object objectField; |
| |
| // Default constructor for deserialization |
| public ExternalizableDataClass() { |
| } |
| |
| // Constructor for serialization |
| public ExternalizableDataClass(byte byteField, Object objectField) { |
| this.byteField = byteField; |
| this.objectField = objectField; |
| } |
| |
| @Override |
| public void writeExternal(ObjectOutput objectOutput) throws IOException { |
| writeInternal(byteField, objectField, objectOutput); |
| } |
| |
| private static void writeInternal(byte b, Object obj, DataOutput out) throws IOException { |
| out.writeByte(b); |
| switch (b) { |
| case TYPE_1: |
| ((Delegate1) obj).delegateWrite(out); |
| break; |
| case TYPE_2: |
| ((Delegate2) obj).delegateWrite(out); |
| break; |
| default: |
| throw new InvalidClassException("Unknown type: " + b); |
| } |
| } |
| |
| @Override |
| public void readExternal(ObjectInput objectInput) throws IOException { |
| byteField = objectInput.readByte(); |
| objectField = readInternal(byteField, objectInput); |
| } |
| |
| private static Object readInternal(byte type, DataInput in) throws IOException { |
| switch (type) { |
| case TYPE_1: return Delegate1.delegateRead(in); |
| case TYPE_2: return Delegate2.delegateRead(in); |
| default: |
| throw new InvalidClassException("Unknown type: " + type); |
| } |
| } |
| |
| private Object readResolve() { |
| return objectField; |
| } |
| |
| @Override |
| public String toString() { |
| return "{ type: " + byteField + ", obj: " + objectField.toString() + " }"; |
| } |
| } |
| |
| final class Delegate1 implements Serializable { |
| private int intField; |
| |
| private Object writeReplace() { |
| return new ExternalizableDataClass(TYPE_1, this); |
| } |
| |
| static Delegate1 of(int intField) { |
| Delegate1 instance = new Delegate1(); |
| instance.intField = intField; |
| return instance; |
| } |
| |
| void delegateWrite(DataOutput out) throws IOException { |
| out.writeInt(intField); |
| } |
| |
| static Object delegateRead(DataInput in) throws IOException { |
| int i = in.readInt(); |
| return Delegate1.of(i); |
| } |
| |
| private Object readResolve() throws ObjectStreamException { |
| throw new InvalidObjectException("Deserialization via serialization delegate"); |
| } |
| |
| @Override |
| public String toString() { |
| return "(" + intField + ")"; |
| } |
| } |
| |
| final class Delegate2 implements Serializable { |
| private String stringField; |
| |
| private Object writeReplace() { |
| return new ExternalizableDataClass(TYPE_2, this); |
| } |
| |
| static Delegate2 of(String stringField) { |
| Delegate2 instance = new Delegate2(); |
| instance.stringField = stringField; |
| return instance; |
| } |
| |
| void delegateWrite(DataOutput out) throws IOException { |
| out.writeUTF(stringField); |
| } |
| |
| static Object delegateRead(DataInput in) throws IOException { |
| String s = in.readUTF(); |
| return Delegate2.of(s); |
| } |
| |
| private Object readResolve() throws ObjectStreamException { |
| throw new InvalidObjectException("Deserialization via serialization delegate"); |
| } |
| |
| @Override |
| public String toString() { |
| return "<" + stringField + ">"; |
| } |
| } |
| |
| class ExternalizableTestMain { |
| public static void main(String[] args) throws Exception { |
| Delegate2 data2 = Delegate2.of("MessageToSerialize"); |
| // "Before: <MessageToSerialize>" |
| System.out.println("Before: " + data2.toString()); |
| |
| // Serialization |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| ObjectOutputStream objectOutputStream = new ObjectOutputStream(out); |
| objectOutputStream.writeObject(data2); |
| objectOutputStream.close(); |
| |
| byte[] byteArray = out.toByteArray(); |
| |
| // Deserialization |
| ByteArrayInputStream in = new ByteArrayInputStream(byteArray); |
| ObjectInputStream objectInputStream = new ObjectInputStream(in); |
| Object copy = objectInputStream.readObject(); |
| assert copy instanceof Delegate2; |
| // "After: <MessageToSerialize>" |
| System.out.println("After: " + copy.toString()); |
| } |
| } |
| |
| class NonSerializableSuperClass { |
| protected String tag; |
| |
| // Default constructor for deserialization |
| public NonSerializableSuperClass() { |
| this.tag = null; |
| } |
| |
| public NonSerializableSuperClass(String tag) { |
| this.tag = tag; |
| } |
| |
| @Override |
| public String toString() { |
| return tag == null ? "NULL" : tag; |
| } |
| } |
| |
| class SerializableDataClass extends NonSerializableSuperClass implements Serializable { |
| private String extraTag; |
| |
| public SerializableDataClass() { |
| super(); |
| this.extraTag = null; |
| } |
| |
| public SerializableDataClass(String tag, String extraTag) { |
| super(tag); |
| this.extraTag = extraTag; |
| } |
| |
| @Override |
| public String toString() { |
| return super.toString() + ", " + (extraTag == null ? "NULL" : extraTag); |
| } |
| } |
| |
| class SerializableTestMain { |
| public static void main(String[] args) throws Exception { |
| SerializableDataClass data = new SerializableDataClass("TagToSerialize", "ExtraToSerialize"); |
| // "Before: TagToSerialize, ExtraToSerialize" |
| System.out.println("Before: " + data.toString()); |
| |
| // Serialization |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| ObjectOutputStream objectOutputStream = new ObjectOutputStream(out); |
| objectOutputStream.writeObject(data); |
| objectOutputStream.close(); |
| |
| byte[] byteArray = out.toByteArray(); |
| |
| // Deserialization |
| ByteArrayInputStream in = new ByteArrayInputStream(byteArray); |
| ObjectInputStream objectInputStream = new ObjectInputStream(in); |
| Object copy = objectInputStream.readObject(); |
| assert copy instanceof SerializableDataClass; |
| // "After: NULL, ExtraToSerialize" |
| System.out.println("After: " + copy.toString()); |
| } |
| } |
| |
| @RunWith(Parameterized.class) |
| public class ExternalizableTest extends ProguardCompatibilityTestBase { |
| private static final List<Class<?>> CLASSES_FOR_EXTERNALIZABLE = |
| ImmutableList.of( |
| ExternalizableDataClass.class, |
| Delegate1.class, |
| Delegate2.class, |
| ExternalizableTestMain.class); |
| |
| private static final List<Class<?>> CLASSES_FOR_SERIALIZABLE = |
| ImmutableList.of( |
| NonSerializableSuperClass.class, SerializableDataClass.class, SerializableTestMain.class); |
| |
| private final Shrinker shrinker; |
| |
| @Parameterized.Parameters(name = "Shrinker: {0}") |
| public static Collection<Object> data() { |
| return ImmutableList.of( |
| Shrinker.PROGUARD6_THEN_D8, Shrinker.PROGUARD6, |
| Shrinker.R8_COMPAT, Shrinker.R8_COMPAT_CF, |
| Shrinker.R8, Shrinker.R8_CF); |
| } |
| |
| public ExternalizableTest(Shrinker shrinker) { |
| this.shrinker = shrinker; |
| } |
| |
| @Test |
| public void testExternalizable() throws Exception { |
| String javaOutput = runOnJava(ExternalizableTestMain.class); |
| |
| List<String> config = ImmutableList.of( |
| keepMainProguardConfiguration(ExternalizableTestMain.class), |
| // https://www.guardsquare.com/en/products/proguard/manual/examples#serializable |
| "-keepclassmembers class * implements java.io.Serializable {", |
| //" private static final java.io.ObjectStreamField[] serialPersistentFields;", |
| " private void writeObject(java.io.ObjectOutputStream);", |
| " private void readObject(java.io.ObjectInputStream);", |
| " java.lang.Object writeReplace();", |
| " java.lang.Object readResolve();", |
| "}"); |
| |
| if (shrinker.isFullModeR8()) { |
| // R8 full mode does not keep the default constructor unless explicitly specified. |
| config = ImmutableList.<String>builder() |
| .addAll(config) |
| .addAll(ImmutableList.of( |
| "-keepclassmembers class * implements java.io.Externalizable {", |
| " public void <init>();", |
| "}" |
| )) |
| .build(); |
| } |
| |
| AndroidApp processedApp = runShrinker(shrinker, CLASSES_FOR_EXTERNALIZABLE, config); |
| |
| // TODO(b/117302947): Need to update ART binary. |
| if (shrinker.generatesCf()) { |
| String output = runOnVM( |
| processedApp, ExternalizableTestMain.class.getCanonicalName(), shrinker.toBackend()); |
| assertEquals(javaOutput.trim(), output.trim()); |
| } |
| |
| // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html |
| // 1.11 The Externalizable Interface |
| // ... |
| // The class of an Externalizable object must do the following: |
| // ... |
| // * Have a public no-arg constructor |
| CodeInspector codeInspector = new CodeInspector(processedApp, proguardMap); |
| ClassSubject classSubject = codeInspector.clazz(ExternalizableDataClass.class); |
| assertThat(classSubject, isPresent()); |
| MethodSubject init = classSubject.init(); |
| assertThat(init, isPresent()); |
| } |
| |
| private void testSerializable(boolean enableVerticalClassMerging) throws Exception { |
| String javaOutput = runOnJava(SerializableTestMain.class); |
| |
| List<String> config = ImmutableList.of( |
| keepMainProguardConfiguration(SerializableTestMain.class), |
| // https://www.guardsquare.com/en/products/proguard/manual/examples#serializable |
| "-keepclassmembers class * implements java.io.Serializable {", |
| //" private static final java.io.ObjectStreamField[] serialPersistentFields;", |
| " private void writeObject(java.io.ObjectOutputStream);", |
| " private void readObject(java.io.ObjectInputStream);", |
| " java.lang.Object writeReplace();", |
| " java.lang.Object readResolve();", |
| "}"); |
| |
| AndroidApp processedApp = |
| runShrinker( |
| shrinker, |
| CLASSES_FOR_SERIALIZABLE, |
| config, |
| options -> |
| options.getVerticalClassMergerOptions().setEnabled(enableVerticalClassMerging)); |
| // TODO(b/117302947): Need to update ART binary. |
| if (shrinker.generatesCf()) { |
| String output = runOnVM( |
| processedApp, SerializableTestMain.class.getCanonicalName(), shrinker.toBackend()); |
| assertEquals(javaOutput.trim(), output.trim()); |
| } |
| |
| // https://docs.oracle.com/javase/8/docs/platform/serialization/spec/serial-arch.html |
| // 1.10 The Serializable Interface |
| // ... |
| // A Serializable class must do the following: |
| // ... |
| // * Have access to the no-arg constructor of its first non-serializable superclass |
| CodeInspector codeInspector = new CodeInspector(processedApp, proguardMap); |
| ClassSubject classSubject = codeInspector.clazz(NonSerializableSuperClass.class); |
| assertThat(classSubject, isPresent()); |
| MethodSubject init = classSubject.init(ImmutableList.of()); |
| assertThat(init, isPresent()); |
| } |
| |
| @Test |
| public void testSerializable_withVerticalClassMerging() throws Exception { |
| if (!shrinker.isR8()) { |
| // Already covered by the other tests. |
| return; |
| } |
| testSerializable(true); |
| } |
| |
| @Test |
| public void testSerializable_withoutVerticalClassMerging() throws Exception { |
| testSerializable(false); |
| } |
| } |