All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowParcel Maven / Gradle / Ivy

package org.robolectric.shadows;

import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.KITKAT_WATCH;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
import static org.robolectric.RuntimeEnvironment.castNativePtr;

import android.os.BadParcelableException;
import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.os.Parcelable.Creator;
import android.util.Log;
import android.util.Pair;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.robolectric.annotation.HiddenApi;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;
import org.robolectric.annotation.Resetter;
import org.robolectric.res.android.NativeObjRegistry;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;

/**
 * Robolectric's {@link Parcel} pretends to be backed by a byte buffer, closely matching {@link
 * Parcel}'s position, size, and capacity behavior. However, its internal pure-Java representation
 * is strongly typed, to detect non-portable code and common testing mistakes. It may throw {@link
 * IllegalArgumentException} or {@link IllegalStateException} for error-prone behavior normal {@link
 * Parcel} tolerates.
 */
@Implements(value = Parcel.class, looseSignatures = true)
public class ShadowParcel {
  protected static final String TAG = "Parcel";

  @RealObject private Parcel realObject;

  private static final NativeObjRegistry NATIVE_BYTE_BUFFER_REGISTRY =
      new NativeObjRegistry<>(ByteBuffer.class);

  private static final HashMap, Class>>>
      pairedCreators = new HashMap<>();

  @Implementation(maxSdk = JELLY_BEAN_MR1)
  @SuppressWarnings("TypeParameterUnusedInFormals")
  protected  T readParcelable(ClassLoader loader) {
    // prior to JB MR2, readParcelableCreator() is inlined here.
    Parcelable.Creator creator = readParcelableCreator(loader);
    if (creator == null) {
      return null;
    }

    if (creator instanceof Parcelable.ClassLoaderCreator) {
      Parcelable.ClassLoaderCreator classLoaderCreator =
          (Parcelable.ClassLoaderCreator) creator;
      return (T) classLoaderCreator.createFromParcel(realObject, loader);
    }
    return (T) creator.createFromParcel(realObject);
  }

  @HiddenApi
  @Implementation(minSdk = JELLY_BEAN_MR2)
  public Parcelable.Creator readParcelableCreator(ClassLoader loader) {
    // note: calling `readString` will also consume the string, and increment the data-pointer.
    // which is exactly what we need, since we do not call the real `readParcelableCreator`.
    final String name = realObject.readString();
    if (name == null) {
      return null;
    }

    Parcelable.Creator creator;
    try {
      // If loader == null, explicitly emulate Class.forName(String) "caller
      // classloader" behavior.
      ClassLoader parcelableClassLoader = (loader == null ? getClass().getClassLoader() : loader);
      // Avoid initializing the Parcelable class until we know it implements
      // Parcelable and has the necessary CREATOR field.
      Class parcelableClass = Class.forName(name, false /* initialize */, parcelableClassLoader);
      if (!Parcelable.class.isAssignableFrom(parcelableClass)) {
        throw new BadParcelableException(
            "Parcelable protocol requires that the " + "class implements Parcelable");
      }
      Field f = parcelableClass.getField("CREATOR");

      // this is a fix for JDK8<->Android VM incompatibility:
      // Apparently, JDK will not allow access to a public field if its
      // class is not visible (private or package-private) from the call-site.
      f.setAccessible(true);

      if ((f.getModifiers() & Modifier.STATIC) == 0) {
        throw new BadParcelableException(
            "Parcelable protocol requires " + "the CREATOR object to be static on class " + name);
      }
      Class creatorType = f.getType();
      if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) {
        // Fail before calling Field.get(), not after, to avoid initializing
        // parcelableClass unnecessarily.
        throw new BadParcelableException(
            "Parcelable protocol requires a "
                + "Parcelable.Creator object called "
                + "CREATOR on class "
                + name);
      }
      creator = (Parcelable.Creator) f.get(null);
    } catch (IllegalAccessException e) {
      Log.e(TAG, "Illegal access when unmarshalling: " + name, e);
      throw new BadParcelableException("IllegalAccessException when unmarshalling: " + name);
    } catch (ClassNotFoundException e) {
      Log.e(TAG, "Class not found when unmarshalling: " + name, e);
      throw new BadParcelableException("ClassNotFoundException when unmarshalling: " + name);
    } catch (NoSuchFieldException e) {
      throw new BadParcelableException(
          "Parcelable protocol requires a "
              + "Parcelable.Creator object called "
              + "CREATOR on class "
              + name);
    }
    if (creator == null) {
      throw new BadParcelableException(
          "Parcelable protocol requires a "
              + "non-null Parcelable.Creator object called "
              + "CREATOR on class "
              + name);
    }
    return creator;
  }

  /**
   * The goal of this shadow method is to workaround a JVM/ART incompatibility.
   *
   * 

In ART, a public field is visible regardless whether or not the enclosing class is public. * On the JVM, this is not the case. For compatibility, we need to use {@link * Field#setAccessible(boolean)} to simulate the same behavior. */ @SuppressWarnings("unchecked") @Implementation(minSdk = TIRAMISU) protected Parcelable.Creator readParcelableCreatorInternal( ClassLoader loader, Class clazz) { String name = realObject.readString(); if (name == null) { return null; } Pair, Class> creatorAndParcelableClass; synchronized (pairedCreators) { HashMap, Class>> map = pairedCreators.get(loader); if (map == null) { pairedCreators.put(loader, new HashMap<>()); creatorAndParcelableClass = null; } else { creatorAndParcelableClass = map.get(name); } } if (creatorAndParcelableClass != null) { Parcelable.Creator creator = creatorAndParcelableClass.first; Class parcelableClass = creatorAndParcelableClass.second; if (clazz != null) { if (!clazz.isAssignableFrom(parcelableClass)) { throw newBadTypeParcelableException( "Parcelable creator " + name + " is not " + "a subclass of required class " + clazz.getName() + " provided in the parameter"); } } return (Parcelable.Creator) creator; } Parcelable.Creator creator; Class parcelableClass; try { // If loader == null, explicitly emulate Class.forName(String) "caller // classloader" behavior. ClassLoader parcelableClassLoader = (loader == null ? getClass().getClassLoader() : loader); // Avoid initializing the Parcelable class until we know it implements // Parcelable and has the necessary CREATOR field. parcelableClass = Class.forName(name, /* initialize= */ false, parcelableClassLoader); if (!Parcelable.class.isAssignableFrom(parcelableClass)) { throw new BadParcelableException( "Parcelable protocol requires subclassing " + "from Parcelable on class " + name); } if (clazz != null) { if (!clazz.isAssignableFrom(parcelableClass)) { throw newBadTypeParcelableException( "Parcelable creator " + name + " is not " + "a subclass of required class " + clazz.getName() + " provided in the parameter"); } } Field f = parcelableClass.getField("CREATOR"); // this is a fix for JDK8<->Android VM incompatibility: // Apparently, JDK will not allow access to a public field if its // class is not visible (private or package-private) from the call-site. f.setAccessible(true); if ((f.getModifiers() & Modifier.STATIC) == 0) { throw new BadParcelableException( "Parcelable protocol requires " + "the CREATOR object to be static on class " + name); } Class creatorType = f.getType(); if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) { // Fail before calling Field.get(), not after, to avoid initializing // parcelableClass unnecessarily. throw new BadParcelableException( "Parcelable protocol requires a " + "Parcelable.Creator object called " + "CREATOR on class " + name); } creator = (Parcelable.Creator) f.get(null); } catch (IllegalAccessException e) { Log.e(TAG, "Illegal access when unmarshalling: " + name, e); throw new BadParcelableException("IllegalAccessException when unmarshalling: " + name, e); } catch (ClassNotFoundException e) { Log.e(TAG, "Class not found when unmarshalling: " + name, e); throw new BadParcelableException("ClassNotFoundException when unmarshalling: " + name, e); } catch (NoSuchFieldException e) { throw new BadParcelableException( "Parcelable protocol requires a " + "Parcelable.Creator object called " + "CREATOR on class " + name, e); } if (creator == null) { throw new BadParcelableException( "Parcelable protocol requires a " + "non-null Parcelable.Creator object called " + "CREATOR on class " + name); } synchronized (pairedCreators) { pairedCreators.get(loader).put(name, Pair.create(creator, parcelableClass)); } return (Parcelable.Creator) creator; } private BadParcelableException newBadTypeParcelableException(String message) { try { return (BadParcelableException) ReflectionHelpers.callConstructor( Class.forName("android.os.BadTypeParcelableException"), ClassParameter.from(String.class, message)); } catch (ClassNotFoundException e) { throw new LinkageError(e.getMessage(), e); } } @Implementation protected void writeByteArray(byte[] b, int offset, int len) { if (b == null) { realObject.writeInt(-1); return; } Number nativePtr = ReflectionHelpers.getField(realObject, "mNativePtr"); nativeWriteByteArray(nativePtr.longValue(), b, offset, len); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static int nativeDataSize(int nativePtr) { return nativeDataSize((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static int nativeDataSize(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).dataSize(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static int nativeDataAvail(int nativePtr) { return nativeDataAvail((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static int nativeDataAvail(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).dataAvailable(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static int nativeDataPosition(int nativePtr) { return nativeDataPosition((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static int nativeDataPosition(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).dataPosition(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static int nativeDataCapacity(int nativePtr) { return nativeDataCapacity((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static int nativeDataCapacity(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).dataCapacity(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeSetDataSize(int nativePtr, int size) { nativeSetDataSize((long) nativePtr, size); } @Implementation(minSdk = LOLLIPOP) @SuppressWarnings("robolectric.ShadowReturnTypeMismatch") protected static void nativeSetDataSize(long nativePtr, int size) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).setDataSize(size); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeSetDataPosition(int nativePtr, int pos) { nativeSetDataPosition((long) nativePtr, pos); } @Implementation(minSdk = LOLLIPOP) protected static void nativeSetDataPosition(long nativePtr, int pos) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).setDataPosition(pos); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeSetDataCapacity(int nativePtr, int size) { nativeSetDataCapacity((long) nativePtr, size); } @Implementation(minSdk = LOLLIPOP) protected static void nativeSetDataCapacity(long nativePtr, int size) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).setDataCapacityAtLeast(size); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeWriteByteArray(int nativePtr, byte[] b, int offset, int len) { nativeWriteByteArray((long) nativePtr, b, offset, len); } @Implementation(minSdk = LOLLIPOP) protected static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeByteArray(b, offset, len); } // duplicate the writeBlob implementation from latest android, to avoid referencing the // non-existent-in-JDK java.util.Arrays.checkOffsetAndCount method. @Implementation(minSdk = M) protected void writeBlob(byte[] b, int offset, int len) { if (b == null) { realObject.writeInt(-1); return; } throwsIfOutOfBounds(b.length, offset, len); long nativePtr = ReflectionHelpers.getField(realObject, "mNativePtr"); nativeWriteBlob(nativePtr, b, offset, len); } private static void throwsIfOutOfBounds(int len, int offset, int count) { if (len < 0) { throw new ArrayIndexOutOfBoundsException("Negative length: " + len); } if ((offset | count) < 0 || offset > len - count) { throw new ArrayIndexOutOfBoundsException(); } } // nativeWriteBlob was introduced in lollipop, thus no need for a int nativePtr variant @Implementation(minSdk = LOLLIPOP) protected static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) { nativeWriteByteArray(nativePtr, b, offset, len); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeWriteInt(int nativePtr, int val) { nativeWriteInt((long) nativePtr, val); } @Implementation(minSdk = LOLLIPOP, maxSdk = R) protected static void nativeWriteInt(long nativePtr, int val) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeInt(val); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeWriteLong(int nativePtr, long val) { nativeWriteLong((long) nativePtr, val); } @Implementation(minSdk = LOLLIPOP, maxSdk = R) protected static void nativeWriteLong(long nativePtr, long val) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeLong(val); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeWriteFloat(int nativePtr, float val) { nativeWriteFloat((long) nativePtr, val); } @Implementation(minSdk = LOLLIPOP, maxSdk = R) protected static void nativeWriteFloat(long nativePtr, float val) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeFloat(val); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeWriteDouble(int nativePtr, double val) { nativeWriteDouble((long) nativePtr, val); } @Implementation(minSdk = LOLLIPOP, maxSdk = R) protected static void nativeWriteDouble(long nativePtr, double val) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeDouble(val); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeWriteString(int nativePtr, String val) { nativeWriteString((long) nativePtr, val); } @Implementation(minSdk = LOLLIPOP, maxSdk = Q) protected static void nativeWriteString(long nativePtr, String val) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeString(val); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) protected static void nativeWriteStrongBinder(int nativePtr, IBinder val) { nativeWriteStrongBinder((long) nativePtr, val); } @Implementation(minSdk = LOLLIPOP) protected static void nativeWriteStrongBinder(long nativePtr, IBinder val) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeStrongBinder(val); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static byte[] nativeCreateByteArray(int nativePtr) { return nativeCreateByteArray((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static byte[] nativeCreateByteArray(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).createByteArray(); } // nativeReadBlob was introduced in lollipop, thus no need for a int nativePtr variant @Implementation(minSdk = LOLLIPOP) protected static byte[] nativeReadBlob(long nativePtr) { return nativeCreateByteArray(nativePtr); } @Implementation(minSdk = O_MR1) protected static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readByteArray(dest, destLen); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static int nativeReadInt(int nativePtr) { return nativeReadInt((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static int nativeReadInt(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readInt(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static long nativeReadLong(int nativePtr) { return nativeReadLong((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static long nativeReadLong(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readLong(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static float nativeReadFloat(int nativePtr) { return nativeReadFloat((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static float nativeReadFloat(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readFloat(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static double nativeReadDouble(int nativePtr) { return nativeReadDouble((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static double nativeReadDouble(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readDouble(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static String nativeReadString(int nativePtr) { return nativeReadString((long) nativePtr); } @Implementation(minSdk = LOLLIPOP, maxSdk = Q) protected static String nativeReadString(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readString(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) protected static IBinder nativeReadStrongBinder(int nativePtr) { return nativeReadStrongBinder((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static IBinder nativeReadStrongBinder(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readStrongBinder(); } @Implementation @HiddenApi public static Number nativeCreate() { return castNativePtr(NATIVE_BYTE_BUFFER_REGISTRY.register(new ByteBuffer())); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeFreeBuffer(int nativePtr) { nativeFreeBuffer((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) @SuppressWarnings("robolectric.ShadowReturnTypeMismatch") protected static void nativeFreeBuffer(long nativePtr) { NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).clear(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeDestroy(int nativePtr) { nativeDestroy((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static void nativeDestroy(long nativePtr) { NATIVE_BYTE_BUFFER_REGISTRY.unregister(nativePtr); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static byte[] nativeMarshall(int nativePtr) { return nativeMarshall((long) nativePtr); } @Implementation(minSdk = LOLLIPOP) protected static byte[] nativeMarshall(long nativePtr) { return NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).toByteArray(); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeUnmarshall(int nativePtr, byte[] data, int offset, int length) { nativeUnmarshall((long) nativePtr, data, offset, length); } @Implementation(minSdk = LOLLIPOP) @SuppressWarnings("robolectric.ShadowReturnTypeMismatch") protected static void nativeUnmarshall(long nativePtr, byte[] data, int offset, int length) { NATIVE_BYTE_BUFFER_REGISTRY.update(nativePtr, ByteBuffer.fromByteArray(data, offset, length)); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeAppendFrom( int thisNativePtr, int otherNativePtr, int offset, int length) { nativeAppendFrom((long) thisNativePtr, otherNativePtr, offset, length); } @Implementation(minSdk = LOLLIPOP) @SuppressWarnings("robolectric.ShadowReturnTypeMismatch") protected static void nativeAppendFrom( long thisNativePtr, long otherNativePtr, int offset, int length) { ByteBuffer thisByteBuffer = NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(thisNativePtr); ByteBuffer otherByteBuffer = NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(otherNativePtr); thisByteBuffer.appendFrom(otherByteBuffer, offset, length); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeWriteInterfaceToken(int nativePtr, String interfaceName) { nativeWriteInterfaceToken((long) nativePtr, interfaceName); } @Implementation(minSdk = LOLLIPOP) protected static void nativeWriteInterfaceToken(long nativePtr, String interfaceName) { // Write StrictMode.ThreadPolicy bits (assume 0 for test). nativeWriteInt(nativePtr, 0); nativeWriteString(nativePtr, interfaceName); } @HiddenApi @Implementation(maxSdk = KITKAT_WATCH) public static void nativeEnforceInterface(int nativePtr, String interfaceName) { nativeEnforceInterface((long) nativePtr, interfaceName); } @Implementation(minSdk = LOLLIPOP) protected static void nativeEnforceInterface(long nativePtr, String interfaceName) { // Consume StrictMode.ThreadPolicy bits (don't bother setting in test). nativeReadInt(nativePtr); String actualInterfaceName = nativeReadString(nativePtr); if (!Objects.equals(interfaceName, actualInterfaceName)) { throw new SecurityException("Binder invocation to an incorrect interface"); } } /** * Robolectric-specific error thrown when tests exercise error-prone behavior in Parcel. * *

Standard Android parcels rarely throw exceptions, but will happily behave in unintended * ways. Parcels are not strongly typed, so will happily re-interpret corrupt contents in ways * that cause hard-to-diagnose failures, or will cause tests to pass when they should not. * ShadowParcel attempts to detect these conditions. * *

This exception is package-private because production or test code should never catch or rely * on this, and may be changed to be an Error (rather than Exception) in the future. */ static class UnreliableBehaviorError extends AssertionError { UnreliableBehaviorError(String message) { super(message); } UnreliableBehaviorError(String message, Throwable cause) { super(message, cause); } UnreliableBehaviorError( Class clazz, int position, ByteBuffer.FakeEncodedItem item, String extraMessage) { super( String.format( Locale.US, "Looking for %s at position %d, found %s [%s] taking %d bytes, %s", clazz.getSimpleName(), position, item.value == null ? "null" : item.value.getClass().getSimpleName(), item.value, item.sizeBytes, extraMessage)); } } /** * ByteBuffer pretends to be the underlying Parcel implementation. * *

It faithfully simulates Parcel's handling of position, size, and capacity, but is strongly * typed internally. It was debated whether this should instead faithfully represent Android's * Parcel bug-for-bug as a true byte array, along with all of its error-tolerant behavior and * ability essentially to {@code reinterpret_cast} data. However, the fail-fast behavior here has * found several test bugs and avoids reliance on architecture-specific details like Endian-ness. * *

Quirky behavior this explicitly emulates: * *

    *
  • Continuing to read past the end returns zeros/nulls. *
  • {@link setDataCapacity} never decreases buffer size. *
  • It is possible to partially or completely overwrite byte ranges in the buffer. *
  • Zero bytes can be exchanged between primitive data types and empty array/string. *
* *

Quirky behavior this forbids: * *

    *
  • Reading past the end after writing without calling setDataPosition(0), since there's no * legitimate reason to do this, and is a very common test bug. *
  • Writing one type and reading another; for example, writing a Long and reading two * Integers, or writing a byte array and reading a String. This, effectively like {@code * reinterpret_cast}, may not be portable across architectures. *
  • Similarly, reading from objects that have been truncated or partially overwritten, or * reading from the middle of them. *
  • Using appendFrom to overwrite data, which in Parcel will overwrite the data and * expand data size by the same amount, introducing empty gaps. *
  • Reading from or marshalling buffers with uninitialized gaps (e.g. where data position was * expanded but nothing was written) *
* *

Possibly-unwanted divergent behavior: * *

    *
  • Reading an object will often return the same instance that was written. *
  • The marshalled form does not at all resemble Parcel's. This is to maintain compatibility * with existing clients that rely on the Java-serialization-based format. *
  • Uses substantially more memory, since each "byte" takes at minimum 4 bytes for a pointer, * and even more for the overhead of allocating a record for each write. But note there is * only at most one allocation for every 4 byte positions. *
*/ private static class ByteBuffer { /** Number of bytes in Parcel used by an int, length, or anything smaller. */ private static final int INT_SIZE_BYTES = 4; /** Number of bytes in Parcel used by a long or double. */ private static final int LONG_OR_DOUBLE_SIZE_BYTES = 8; /** Immutable empty byte array. */ private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; /** Representation for an item that has been serialized in a parcel. */ private static class FakeEncodedItem implements Serializable { /** Number of consecutive bytes consumed by this object. */ final int sizeBytes; /** The original typed value stored. */ final Object value; /** * Whether this item's byte-encoding is all zero. * *

This is the one exception to strong typing in ShadowParcel. Since zero can be portably * handled by many primitive types as zeros, and strings and arrays as empty. Note that when * zeroes are successfully read, the size of this entry may be ignored and the position may * progress to the middle of this, which remains safe as long as types that handle zeros are * used. */ final boolean isEncodedAsAllZeroBytes; FakeEncodedItem(int sizeBytes, Object value) { this.sizeBytes = sizeBytes; this.value = value; this.isEncodedAsAllZeroBytes = isEncodedAsAllZeroBytes(value); } } /** * A type-safe simulation of the Parcel's data buffer. * *

Each index represents a byte of the parcel. Instead of storing raw bytes, this contains * records containing both the original data (in its original Java type) as well as the length. * Consecutive indices will point to the same FakeEncodedItem instance; for example, an item * with sizeBytes of 24 will, in normal cases, have references from 24 consecutive indices. * *

There are two main fail-fast features in this type-safe buffer. First, objects may only be * read from the parcel as the same type they were stored with, enforced by casting. Second, * this fails fast when reading incomplete or partially overwritten items. * *

Even though writing a custom resizable array is a code smell vs ArrayList, arrays' fixed * capacity closely models Parcel's dataCapacity (which we emulate anyway), and bulk array * utilities are robust compared to ArrayList's bulk operations. */ private FakeEncodedItem[] data; /** The read/write pointer. */ private int dataPosition; /** The length of the buffer; the capacity is data.length. */ private int dataSize; /** * Whether the next read should fail if it's past the end of the array. * *

This is set true when modifying the end of the buffer, and cleared if a data position was * explicitly set. */ private boolean failNextReadIfPastEnd; ByteBuffer() { clear(); } /** Removes all elements from the byte buffer */ public void clear() { data = new FakeEncodedItem[0]; dataPosition = 0; dataSize = 0; failNextReadIfPastEnd = false; } /** Reads a byte array from the byte buffer based on the current data position */ public byte[] createByteArray() { // It would be simpler just to store the byte array without a separate length. However, the // "non-native" code in Parcel short-circuits null to -1, so this must consistently write a // separate length field in all cases. int length = readInt(); if (length == -1) { return null; } if (length == 0) { return EMPTY_BYTE_ARRAY; } Object current = peek(); if (current instanceof Byte) { // Legacy-encoded byte arrays (created by some tests) encode individual bytes, and do not // align to the integer. return readLegacyByteArray(length); } else if (readZeroes(alignToInt(length))) { return new byte[length]; } byte[] result = readValue(EMPTY_BYTE_ARRAY, byte[].class, /* allowNull= */ false); if (result.length != length) { // Looks like the length doesn't correspond to the array. throw new UnreliableBehaviorError( String.format( Locale.US, "Byte array's length prefix is %d but real length is %d", length, result.length)); } return result; } /** Reads a byte array encoded the way ShadowParcel previously encoded byte arrays. */ private byte[] readLegacyByteArray(int length) { // Some tests rely on ShadowParcel's previous byte-by-byte encoding. byte[] result = new byte[length]; for (int i = 0; i < length; i++) { result[i] = readPrimitive(1, (byte) 0, Byte.class); } return result; } /** Reads a byte array from the byte buffer based on the current data position */ public boolean readByteArray(byte[] dest, int destLen) { byte[] result = createByteArray(); if (result == null || destLen != result.length) { // Since older versions of Android (pre O MR1) don't call this method at all, let's be more // consistent with them and let android.os.Parcel throw RuntimeException, instead of // throwing a more helpful exception. return false; } System.arraycopy(result, 0, dest, 0, destLen); return true; } /** * Writes a byte array starting at offset for length bytes to the byte buffer at the current * data position */ public void writeByteArray(byte[] b, int offset, int length) { writeInt(length); // Native parcel writes a byte array as length plus the individual bytes. But we can't write // bytes individually because each byte would take up 4 bytes due to Parcel's alignment // behavior. Instead we write the length, and if non-empty, we write the array. if (length != 0) { writeValue(length, Arrays.copyOfRange(b, offset, offset + length)); } } /** Writes an int to the byte buffer at the current data position */ public void writeInt(int i) { writeValue(INT_SIZE_BYTES, i); } /** Reads a int from the byte buffer based on the current data position */ public int readInt() { return readPrimitive(INT_SIZE_BYTES, 0, Integer.class); } /** Writes a long to the byte buffer at the current data position */ public void writeLong(long l) { writeValue(LONG_OR_DOUBLE_SIZE_BYTES, l); } /** Reads a long from the byte buffer based on the current data position */ public long readLong() { return readPrimitive(LONG_OR_DOUBLE_SIZE_BYTES, 0L, Long.class); } /** Writes a float to the byte buffer at the current data position */ public void writeFloat(float f) { writeValue(INT_SIZE_BYTES, f); } /** Reads a float from the byte buffer based on the current data position */ public float readFloat() { return readPrimitive(INT_SIZE_BYTES, 0f, Float.class); } /** Writes a double to the byte buffer at the current data position */ public void writeDouble(double d) { writeValue(LONG_OR_DOUBLE_SIZE_BYTES, d); } /** Reads a double from the byte buffer based on the current data position */ public double readDouble() { return readPrimitive(LONG_OR_DOUBLE_SIZE_BYTES, 0d, Double.class); } /** Writes a String to the byte buffer at the current data position */ public void writeString(String s) { int nullTerminatedChars = (s != null) ? (s.length() + 1) : 0; // Android encodes strings as length plus a null-terminated array of 2-byte characters. // writeValue will pad to nearest 4 bytes. Null is encoded as just -1. int sizeBytes = INT_SIZE_BYTES + (nullTerminatedChars * 2); writeValue(sizeBytes, s); } /** Reads a String from the byte buffer based on the current data position */ public String readString() { if (readZeroes(INT_SIZE_BYTES * 2)) { // Empty string is 4 bytes for length of 0, and 4 bytes for null terminator and padding. return ""; } return readValue(null, String.class, /* allowNull= */ true); } /** Writes an IBinder to the byte buffer at the current data position */ public void writeStrongBinder(IBinder b) { // Size of struct flat_binder_object in android/binder.h used to encode binders in the real // parceling code. int length = 5 * INT_SIZE_BYTES; writeValue(length, b); } /** Reads an IBinder from the byte buffer based on the current data position */ public IBinder readStrongBinder() { return readValue(null, IBinder.class, /* allowNull= */ true); } /** * Appends the contents of the other byte buffer to this byte buffer starting at offset and * ending at length. * * @param other ByteBuffer to append to this one * @param offset number of bytes from beginning of byte buffer to start copy from * @param length number of bytes to copy */ public void appendFrom(ByteBuffer other, int offset, int length) { int oldSize = dataSize; if (dataPosition != dataSize) { // Parcel.cpp will always expand the buffer by length even if it is overwriting existing // data, yielding extra uninitialized data at the end, in contrast to write methods that // won't increase the data length if they are overwriting in place. This is surprising // behavior that production code should avoid. throw new UnreliableBehaviorError( "Real Android parcels behave unreliably if appendFrom is " + "called from any position other than the end"); } setDataSize(oldSize + length); // Just blindly copy whatever happens to be in the buffer. Reads will validate whether any // of the objects were only incompletely copied. System.arraycopy(other.data, offset, data, dataPosition, length); dataPosition += length; failNextReadIfPastEnd = true; } /** Returns whether a data type is encoded as all zeroes. */ private static boolean isEncodedAsAllZeroBytes(Object value) { if (value == null) { return false; // Nulls are usually encoded as -1. } if (value instanceof Number) { Number number = (Number) value; return number.longValue() == 0 && number.doubleValue() == 0; } if (value instanceof byte[]) { byte[] array = (byte[]) value; return isAllZeroes(array, 0, array.length); } // NOTE: While empty string is all zeros, trying to read an empty string as zeroes is // probably unintended; the reverse is supported just so all-zero buffers don't fail. return false; } /** Identifies all zeroes, which can be safely reinterpreted to other types. */ private static boolean isAllZeroes(byte[] array, int offset, int length) { for (int i = offset; i < length; i++) { if (array[i] != 0) { return false; } } return true; } /** * Creates a Byte buffer from a raw byte array. * * @param array byte array to read from * @param offset starting position in bytes to start reading array at * @param length number of bytes to read from array */ @SuppressWarnings("BanSerializableRead") public static ByteBuffer fromByteArray(byte[] array, int offset, int length) { ByteBuffer byteBuffer = new ByteBuffer(); if (isAllZeroes(array, offset, length)) { // Special case: for all zeroes, it's definitely not an ObjectInputStream, because it has a // non-zero mandatory magic. Zeroes have a portable, unambiguous interpretation. byteBuffer.setDataSize(length); byteBuffer.writeItem(new FakeEncodedItem(length, new byte[length])); return byteBuffer; } try { ByteArrayInputStream bis = new ByteArrayInputStream(array, offset, length); ObjectInputStream ois = new ObjectInputStream(bis); int numElements = ois.readInt(); for (int i = 0; i < numElements; i++) { int sizeOf = ois.readInt(); Object value = ois.readObject(); // NOTE: Bypassing writeValue so that this will support ShadowParcels that were // marshalled before ShadowParcel simulated alignment. byteBuffer.writeItem(new FakeEncodedItem(sizeOf, value)); } // Android leaves the data position at the end in this case. return byteBuffer; } catch (Exception e) { throw new UnreliableBehaviorError("ShadowParcel unable to unmarshall its custom format", e); } } /** * Converts a ByteBuffer to a raw byte array. This method should be symmetrical with * fromByteArray. */ public byte[] toByteArray() { int oldDataPosition = dataPosition; try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); // NOTE: Serializing the data array would be simpler, and serialization would actually // preserve reference equality between entries. However, the length-encoded format here // preserves the previous format, which some tests appear to rely on. List entries = new ArrayList<>(); // NOTE: Use readNextItem to scan so the contents can be proactively validated. dataPosition = 0; while (dataPosition < dataSize) { entries.add(readNextItem(Object.class)); } oos.writeInt(entries.size()); for (FakeEncodedItem item : entries) { oos.writeInt(item.sizeBytes); oos.writeObject(item.value); } oos.flush(); return bos.toByteArray(); } catch (IOException e) { throw new UnreliableBehaviorError("ErrorProne unable to serialize its custom format", e); } finally { dataPosition = oldDataPosition; } } /** Number of unused bytes in this byte buffer. */ public int dataAvailable() { return dataSize() - dataPosition(); } /** Total buffer size in bytes of byte buffer included unused space. */ public int dataCapacity() { return data.length; } /** Current data position of byte buffer in bytes. Reads / writes are from this position. */ public int dataPosition() { return dataPosition; } /** Current amount of bytes currently written for ByteBuffer. */ public int dataSize() { return dataSize; } /** * Sets the current data position. * * @param pos Desired position in bytes */ public void setDataPosition(int pos) { if (pos > dataSize) { // NOTE: Real parcel ignores this until a write occurs. throw new UnreliableBehaviorError(pos + " greater than dataSize " + dataSize); } dataPosition = pos; failNextReadIfPastEnd = false; } public void setDataSize(int size) { if (size < dataSize) { // Clear all the inaccessible bytes when shrinking, to allow garbage collection, and so // they remain cleared if expanded again. Note this might truncate something mid-object, // which would be handled at read time. Arrays.fill(data, size, dataSize, null); } setDataCapacityAtLeast(size); dataSize = size; if (dataPosition >= dataSize) { dataPosition = dataSize; } } public void setDataCapacityAtLeast(int newCapacity) { // NOTE: Oddly, Parcel only every increases data capacity, and never decreases it, so this // really should have never been named setDataCapacity. if (newCapacity > data.length) { FakeEncodedItem[] newData = new FakeEncodedItem[newCapacity]; dataSize = Math.min(dataSize, newCapacity); dataPosition = Math.min(dataPosition, dataSize); System.arraycopy(data, 0, newData, 0, dataSize); data = newData; } } /** Rounds to next 4-byte bounder similar to native Parcel. */ private int alignToInt(int unpaddedSizeBytes) { return ((unpaddedSizeBytes + 3) / 4) * 4; } /** * Ensures that the next sizeBytes are all the initial value we read. * *

This detects: * *

    *
  • Reading an item, but not starting at its start position *
  • Reading items that were truncated by setSize *
  • Reading items that were partially overwritten by another *
*/ private void checkConsistentReadAndIncrementPosition(Class clazz, FakeEncodedItem item) { int endPosition = dataPosition + item.sizeBytes; for (int i = dataPosition; i < endPosition; i++) { FakeEncodedItem foundItemItem = i < dataSize ? data[i] : null; if (foundItemItem != item) { throw new UnreliableBehaviorError( clazz, dataPosition, item, String.format( Locale.US, "but [%s] interrupts it at position %d", foundItemItem == null ? "uninitialized data or the end of the buffer" : foundItemItem.value, i)); } } dataPosition = Math.min(dataSize, dataPosition + item.sizeBytes); } /** Returns the item at the current position, or null if uninitialized or null. */ private Object peek() { return dataPosition < dataSize && data[dataPosition] != null ? data[dataPosition].value : null; } /** * Reads a complete item in the byte buffer. * * @param clazz this is the type that is being read, but not checked in this method * @return null if the default value should be returned, otherwise the item holding the data */ private FakeEncodedItem readNextItem(Class clazz) { FakeEncodedItem item = data[dataPosition]; if (item == null) { // While Parcel will treat these as zeros, in tests, this is almost always an error. throw new UnreliableBehaviorError("Reading uninitialized data at position " + dataPosition); } checkConsistentReadAndIncrementPosition(clazz, item); return item; } /** * Reads the next value in the byte buffer of a specified type. * * @param pastEndValue value to return when reading past the end of the buffer * @param clazz this is the type that is being read, but not checked in this method * @param allowNull whether null values are permitted */ private T readValue(T pastEndValue, Class clazz, boolean allowNull) { if (dataPosition >= dataSize) { // Normally, reading past the end is permitted, and returns the default values. However, // writing to a parcel then reading without setting the position back to 0 is an incredibly // common error to make in tests, and should never really happen in production code, so // this shadow will fail in this condition. if (failNextReadIfPastEnd) { throw new UnreliableBehaviorError( "Did you forget to setDataPosition(0) before reading the parcel?"); } return pastEndValue; } int startPosition = dataPosition; FakeEncodedItem item = readNextItem(clazz); if (item == null) { return pastEndValue; } else if (item.value == null && allowNull) { return null; } else if (clazz.isInstance(item.value)) { return clazz.cast(item.value); } else { // Numerous existing tests rely on ShadowParcel throwing RuntimeException and catching // them. Many of these tests are trying to test what happens when an invalid Parcel is // provided. However, Android has no concept of an "invalid parcel" because Parcel will // happily return garbage if you ask for it. The only runtime exceptions are thrown on // array length mismatches, or higher-level APIs like Parcelable (which has its own safety // checks). Tests trying to test error-handling behavior should instead craft a Parcel // that specifically triggers a BadParcelableException. throw new RuntimeException( new UnreliableBehaviorError( clazz, startPosition, item, "and it is non-portable to reinterpret it")); } } /** * Determines if there is a sequence of castable zeroes, and consumes them. * *

This is the only exception for strong typing, because zero bytes are portable and * unambiguous. There are a few situations where well-written code can rely on this, so it is * worthwhile making a special exception for. This tolerates partially-overwritten and truncated * values if all bytes are zero. */ private boolean readZeroes(int bytes) { int endPosition = dataPosition + bytes; if (endPosition > dataSize) { return false; } for (int i = dataPosition; i < endPosition; i++) { if (data[i] == null || !data[i].isEncodedAsAllZeroBytes) { return false; } } // Note in this case we short-circuit other verification -- even if we are reading weirdly // clobbered zeroes, they're still zeroes. Future reads might fail, though. dataPosition = endPosition; return true; } /** * Reads a primitive, which may reinterpret zeros of other types. * * @param defaultSizeBytes if reinterpreting zeros, the number of bytes to consume * @param defaultValue the default value for zeros or reading past the end * @param clazz this is the type that is being read, but not checked in this method */ private T readPrimitive(int defaultSizeBytes, T defaultValue, Class clazz) { // Check for zeroes first, since partially-overwritten values are not an error for zeroes. if (readZeroes(defaultSizeBytes)) { return defaultValue; } return readValue(defaultValue, clazz, /* allowNull= */ false); } /** Writes an encoded item directly, bypassing alignment, and possibly repeating an item. */ private void writeItem(FakeEncodedItem item) { int endPosition = dataPosition + item.sizeBytes; if (endPosition > data.length) { // Parcel grows by 3/2 of the new size. setDataCapacityAtLeast(endPosition * 3 / 2); } if (endPosition > dataSize) { failNextReadIfPastEnd = true; dataSize = endPosition; } Arrays.fill(data, dataPosition, endPosition, item); dataPosition = endPosition; } /** * Writes a value to the next range of bytes. * *

Writes are aligned to 4-byte regions. */ private void writeValue(int unpaddedSizeBytes, Object o) { // Create the item with its final, aligned byte size. writeItem(new FakeEncodedItem(alignToInt(unpaddedSizeBytes), o)); } } @Implementation(maxSdk = P) protected static FileDescriptor openFileDescriptor(String file, int mode) throws IOException { RandomAccessFile randomAccessFile = new RandomAccessFile(file, mode == ParcelFileDescriptor.MODE_READ_ONLY ? "r" : "rw"); return randomAccessFile.getFD(); } @Implementation(minSdk = M, maxSdk = R) protected static long nativeWriteFileDescriptor(long nativePtr, FileDescriptor val) { // The Java version of FileDescriptor stored the fd in a field called "fd", and the Android // version changed the field name to "descriptor". But it looks like Robolectric uses the // Java version of FileDescriptor instead of the Android version. int fd = ReflectionHelpers.getField(val, "fd"); NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).writeInt(fd); return (long) nativeDataPosition(nativePtr); } @Implementation(minSdk = M) protected static FileDescriptor nativeReadFileDescriptor(long nativePtr) { int fd = NATIVE_BYTE_BUFFER_REGISTRY.getNativeObject(nativePtr).readInt(); return ReflectionHelpers.callConstructor( FileDescriptor.class, ClassParameter.from(int.class, fd)); } @Implementation(minSdk = R) protected static void nativeWriteString8(long nativePtr, String val) { nativeWriteString(nativePtr, val); } @Implementation(minSdk = R) protected static void nativeWriteString16(long nativePtr, String val) { nativeWriteString(nativePtr, val); } @Implementation(minSdk = R) protected static String nativeReadString8(long nativePtr) { return nativeReadString(nativePtr); } @Implementation(minSdk = R) protected static String nativeReadString16(long nativePtr) { return nativeReadString(nativePtr); } // need to use looseSignatures for the S methods because method signatures differ only by return // type @Implementation(minSdk = S) protected static int nativeWriteInt(Object nativePtr, Object val) { nativeWriteInt((long) nativePtr, (int) val); return 0; /* OK */ } @Implementation(minSdk = S) protected static int nativeWriteLong(Object nativePtr, Object val) { nativeWriteLong((long) nativePtr, (long) val); return 0; /* OK */ } @Implementation(minSdk = S) protected static int nativeWriteFloat(Object nativePtr, Object val) { nativeWriteFloat((long) nativePtr, (float) val); return 0; /* OK */ } @Implementation(minSdk = S) protected static int nativeWriteDouble(Object nativePtr, Object val) { nativeWriteDouble((long) nativePtr, (double) val); return 0; /* OK */ } @Implementation(minSdk = S) protected static void nativeWriteFileDescriptor(Object nativePtr, Object val) { nativeWriteFileDescriptor((long) nativePtr, (FileDescriptor) val); } @Resetter public static void reset() { pairedCreators.clear(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy