diff options
| -rw-r--r-- | core/java/android/ddm/DdmHandleViewDebug.java | 297 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java | 398 |
2 files changed, 632 insertions, 63 deletions
diff --git a/core/java/android/ddm/DdmHandleViewDebug.java b/core/java/android/ddm/DdmHandleViewDebug.java index 6b0f78f2c1c3..0f66fcbdbec9 100644 --- a/core/java/android/ddm/DdmHandleViewDebug.java +++ b/core/java/android/ddm/DdmHandleViewDebug.java @@ -16,12 +16,16 @@ package android.ddm; +import static com.android.internal.util.Preconditions.checkArgument; + import android.util.Log; import android.view.View; import android.view.ViewDebug; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; +import com.android.internal.annotations.VisibleForTesting; + import org.apache.harmony.dalvik.ddmc.Chunk; import org.apache.harmony.dalvik.ddmc.ChunkHandler; import org.apache.harmony.dalvik.ddmc.DdmServer; @@ -34,6 +38,7 @@ import java.io.OutputStreamWriter; import java.lang.reflect.Method; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; /** * Handle various requests related to profiling / debugging of the view system. @@ -123,14 +128,15 @@ public class DdmHandleViewDebug extends DdmHandle { } if (type == CHUNK_VURT) { - if (op == VURT_DUMP_HIERARCHY) + if (op == VURT_DUMP_HIERARCHY) { return dumpHierarchy(rootView, in); - else if (op == VURT_CAPTURE_LAYERS) + } else if (op == VURT_CAPTURE_LAYERS) { return captureLayers(rootView); - else if (op == VURT_DUMP_THEME) + } else if (op == VURT_DUMP_THEME) { return dumpTheme(rootView); - else + } else { return createFailChunk(ERR_INVALID_OP, "Unknown view root operation: " + op); + } } final View targetView = getTargetView(rootView, in); @@ -207,9 +213,9 @@ public class DdmHandleViewDebug extends DdmHandle { /** * Returns the view hierarchy and/or view properties starting at the provided view. * Based on the input options, the return data may include: - * - just the view hierarchy - * - view hierarchy & the properties for each of the views - * - just the view properties for a specific view. + * - just the view hierarchy + * - view hierarchy & the properties for each of the views + * - just the view properties for a specific view. * TODO: Currently this only returns views starting at the root, need to fix so that * it can return properties of any view. */ @@ -220,7 +226,7 @@ public class DdmHandleViewDebug extends DdmHandle { long start = System.currentTimeMillis(); - ByteArrayOutputStream b = new ByteArrayOutputStream(2*1024*1024); + ByteArrayOutputStream b = new ByteArrayOutputStream(2 * 1024 * 1024); try { if (v2) { ViewDebug.dumpv2(rootView, b); @@ -304,17 +310,47 @@ public class DdmHandleViewDebug extends DdmHandle { * Invokes provided method on the view. * The method name and its arguments are passed in as inputs via the byte buffer. * The buffer contains:<ol> - * <li> len(method name) </li> - * <li> method name </li> - * <li> # of args </li> - * <li> arguments: Each argument comprises of a type specifier followed by the actual argument. - * The type specifier is a single character as used in JNI: - * (Z - boolean, B - byte, C - char, S - short, I - int, J - long, - * F - float, D - double). <p> - * The type specifier is followed by the actual value of argument. - * Booleans are encoded via bytes with 0 indicating false.</li> + * <li> len(method name) </li> + * <li> method name (encoded as UTF-16 2-byte characters) </li> + * <li> # of args </li> + * <li> arguments: Each argument comprises of a type specifier followed by the actual argument. + * The type specifier is one character modelled after JNI signatures: + * <ul> + * <li>[ - array<br> + * This is followed by a second character according to this spec, indicating the + * array type, then the array length as an Int, followed by a repeated encoding + * of the actual data. + * WARNING: Only <b>byte[]</b> is supported currently. + * </li> + * <li>Z - boolean<br> + * Booleans are encoded via bytes with 0 indicating false</li> + * <li>B - byte</li> + * <li>C - char</li> + * <li>S - short</li> + * <li>I - int</li> + * <li>J - long</li> + * <li>F - float</li> + * <li>D - double</li> + * <li>V - void<br> + * NOT followed by a value. Only used for return types</li> + * <li>R - String (not a real JNI type, but added for convenience)<br> + * Strings are encoded as an unsigned short of the number of <b>bytes</b>, + * followed by the actual UTF-8 encoded bytes. + * WARNING: This is the same encoding as produced by + * ViewHierarchyEncoder#writeString. However, note that this encoding is + * different to what DdmHandle#getString() expects, which is used in other places + * in this class. + * WARNING: Since the length is the number of UTF-8 encoded bytes, Strings can + * contain up to 64k ASCII characters, yet depending on the actual data, the true + * maximum might be as little as 21844 unicode characters. + * <b>null</b> String objects are encoded as an empty string + * </li> + * </ul> + * </li> * </ol> * Methods that take no arguments need only specify the method name. + * + * The return value is encoded the same way as a single parameter (type + value) */ private Chunk invokeViewMethod(final View rootView, final View targetView, ByteBuffer in) { int l = in.getInt(); @@ -327,54 +363,17 @@ public class DdmHandleViewDebug extends DdmHandle { args = new Object[0]; } else { int nArgs = in.getInt(); - argTypes = new Class<?>[nArgs]; args = new Object[nArgs]; - for (int i = 0; i < nArgs; i++) { - char c = in.getChar(); - switch (c) { - case 'Z': - argTypes[i] = boolean.class; - args[i] = in.get() == 0 ? false : true; - break; - case 'B': - argTypes[i] = byte.class; - args[i] = in.get(); - break; - case 'C': - argTypes[i] = char.class; - args[i] = in.getChar(); - break; - case 'S': - argTypes[i] = short.class; - args[i] = in.getShort(); - break; - case 'I': - argTypes[i] = int.class; - args[i] = in.getInt(); - break; - case 'J': - argTypes[i] = long.class; - args[i] = in.getLong(); - break; - case 'F': - argTypes[i] = float.class; - args[i] = in.getFloat(); - break; - case 'D': - argTypes[i] = double.class; - args[i] = in.getDouble(); - break; - default: - Log.e(TAG, "arg " + i + ", unrecognized type: " + c); - return createFailChunk(ERR_INVALID_PARAM, - "Unsupported parameter type (" + c + ") to invoke view method."); - } + try { + deserializeMethodParameters(args, argTypes, in); + } catch (ViewMethodInvocationSerializationException e) { + return createFailChunk(ERR_INVALID_PARAM, e.getMessage()); } } - Method method = null; + Method method; try { method = targetView.getClass().getMethod(methodName, argTypes); } catch (NoSuchMethodException e) { @@ -384,7 +383,10 @@ public class DdmHandleViewDebug extends DdmHandle { } try { - ViewDebug.invokeViewMethod(targetView, method, args); + Object result = ViewDebug.invokeViewMethod(targetView, method, args); + Class<?> returnType = method.getReturnType(); + byte[] returnValue = serializeReturnValue(returnType, returnType.cast(result)); + return new Chunk(CHUNK_VUOP, returnValue, 0, returnValue.length); } catch (Exception e) { Log.e(TAG, "Exception while invoking method: " + e.getCause().getMessage()); String msg = e.getCause().getMessage(); @@ -393,8 +395,6 @@ public class DdmHandleViewDebug extends DdmHandle { } return createFailChunk(ERR_EXCEPTION, msg); } - - return null; } private Chunk setLayoutParameter(final View rootView, final View targetView, ByteBuffer in) { @@ -406,7 +406,7 @@ public class DdmHandleViewDebug extends DdmHandle { } catch (Exception e) { Log.e(TAG, "Exception setting layout parameter: " + e); return createFailChunk(ERR_EXCEPTION, "Error accessing field " - + param + ":" + e.getMessage()); + + param + ":" + e.getMessage()); } return null; @@ -431,4 +431,175 @@ public class DdmHandleViewDebug extends DdmHandle { byte[] data = b.toByteArray(); return new Chunk(CHUNK_VUOP, data, 0, data.length); } + + /** + * Deserializes parameters according to the VUOP_INVOKE_VIEW_METHOD protocol the {@code in} + * buffer. + * + * The length of {@code args} determines how many arguments are read. The {@code argTypes} must + * be the same length, and will be set to the argument types of the data read. + * + * @hide + */ + @VisibleForTesting + public static void deserializeMethodParameters( + Object[] args, Class<?>[] argTypes, ByteBuffer in) throws + ViewMethodInvocationSerializationException { + checkArgument(args.length == argTypes.length); + + for (int i = 0; i < args.length; i++) { + char typeSignature = in.getChar(); + boolean isArray = typeSignature == SIG_ARRAY; + if (isArray) { + char arrayType = in.getChar(); + if (arrayType != SIG_BYTE) { + // This implementation only supports byte-arrays for now. + throw new ViewMethodInvocationSerializationException( + "Unsupported array parameter type (" + typeSignature + + ") to invoke view method @argument " + i); + } + + int arrayLength = in.getInt(); + if (arrayLength > in.remaining()) { + // The sender did not actually sent the specified amount of bytes. This + // avoids a malformed packet to trigger an out-of-memory error. + throw new BufferUnderflowException(); + } + + byte[] byteArray = new byte[arrayLength]; + in.get(byteArray); + + argTypes[i] = byte[].class; + args[i] = byteArray; + } else { + switch (typeSignature) { + case SIG_BOOLEAN: + argTypes[i] = boolean.class; + args[i] = in.get() != 0; + break; + case SIG_BYTE: + argTypes[i] = byte.class; + args[i] = in.get(); + break; + case SIG_CHAR: + argTypes[i] = char.class; + args[i] = in.getChar(); + break; + case SIG_SHORT: + argTypes[i] = short.class; + args[i] = in.getShort(); + break; + case SIG_INT: + argTypes[i] = int.class; + args[i] = in.getInt(); + break; + case SIG_LONG: + argTypes[i] = long.class; + args[i] = in.getLong(); + break; + case SIG_FLOAT: + argTypes[i] = float.class; + args[i] = in.getFloat(); + break; + case SIG_DOUBLE: + argTypes[i] = double.class; + args[i] = in.getDouble(); + break; + case SIG_STRING: { + argTypes[i] = String.class; + int stringUtf8ByteCount = Short.toUnsignedInt(in.getShort()); + byte[] rawStringBuffer = new byte[stringUtf8ByteCount]; + in.get(rawStringBuffer); + args[i] = new String(rawStringBuffer, StandardCharsets.UTF_8); + break; + } + default: + Log.e(TAG, "arg " + i + ", unrecognized type: " + typeSignature); + throw new ViewMethodInvocationSerializationException( + "Unsupported parameter type (" + typeSignature + + ") to invoke view method."); + } + } + + } + } + + /** + * Serializes {@code value} to the wire protocol of VUOP_INVOKE_VIEW_METHOD. + * @hide + */ + @VisibleForTesting + public static byte[] serializeReturnValue(Class<?> type, Object value) + throws ViewMethodInvocationSerializationException, IOException { + ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(byteOutStream); + + if (type.isArray()) { + if (!type.equals(byte[].class)) { + // Only byte arrays are supported currently. + throw new ViewMethodInvocationSerializationException( + "Unsupported array return type (" + type + ")"); + } + byte[] byteArray = (byte[]) value; + dos.writeChar(SIG_ARRAY); + dos.writeChar(SIG_BYTE); + dos.writeInt(byteArray.length); + dos.write(byteArray); + } else if (boolean.class.equals(type)) { + dos.writeChar(SIG_BOOLEAN); + dos.write((boolean) value ? 1 : 0); + } else if (byte.class.equals(type)) { + dos.writeChar(SIG_BYTE); + dos.writeByte((byte) value); + } else if (char.class.equals(type)) { + dos.writeChar(SIG_CHAR); + dos.writeChar((char) value); + } else if (short.class.equals(type)) { + dos.writeChar(SIG_SHORT); + dos.writeShort((short) value); + } else if (int.class.equals(type)) { + dos.writeChar(SIG_INT); + dos.writeInt((int) value); + } else if (long.class.equals(type)) { + dos.writeChar(SIG_LONG); + dos.writeLong((long) value); + } else if (double.class.equals(type)) { + dos.writeChar(SIG_DOUBLE); + dos.writeDouble((double) value); + } else if (float.class.equals(type)) { + dos.writeChar(SIG_FLOAT); + dos.writeFloat((float) value); + } else if (String.class.equals(type)) { + dos.writeChar(SIG_STRING); + dos.writeUTF(value != null ? (String) value : ""); + } else { + dos.writeChar(SIG_VOID); + } + + return byteOutStream.toByteArray(); + } + + // Prefixes for simple primitives. These match the JNI definitions. + private static final char SIG_ARRAY = '['; + private static final char SIG_BOOLEAN = 'Z'; + private static final char SIG_BYTE = 'B'; + private static final char SIG_SHORT = 'S'; + private static final char SIG_CHAR = 'C'; + private static final char SIG_INT = 'I'; + private static final char SIG_LONG = 'J'; + private static final char SIG_FLOAT = 'F'; + private static final char SIG_DOUBLE = 'D'; + private static final char SIG_VOID = 'V'; + // Prefixes for some commonly used objects + private static final char SIG_STRING = 'R'; + + /** + * @hide + */ + @VisibleForTesting + public static class ViewMethodInvocationSerializationException extends Exception { + ViewMethodInvocationSerializationException(String message) { + super(message); + } + } } diff --git a/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java b/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java new file mode 100644 index 000000000000..7248983c741c --- /dev/null +++ b/core/tests/coretests/src/android/ddm/DdmHandleViewDebugTest.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.ddm; + +import static android.ddm.DdmHandleViewDebug.deserializeMethodParameters; +import static android.ddm.DdmHandleViewDebug.serializeReturnValue; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import android.ddm.DdmHandleViewDebug.ViewMethodInvocationSerializationException; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +@SmallTest +@Presubmit +public final class DdmHandleViewDebugTest { + // true + private static final byte[] SERIALIZED_BOOLEAN_TRUE = {0x00, 0x5A, 1}; + + @Test + public void serializeReturnValue_booleanTrue() throws Exception { + assertArrayEquals(SERIALIZED_BOOLEAN_TRUE, serializeReturnValue(boolean.class, true)); + } + + @Test + public void deserializeMethodParameters_booleanTrue() throws Exception { + expectDeserializedArgument(boolean.class, true, SERIALIZED_BOOLEAN_TRUE); + } + + // false + private static final byte[] SERIALIZED_BOOLEAN_FALSE = {0x00, 0x5A, 0}; + + @Test + public void serializeReturnValue_booleanFalse() throws Exception { + assertArrayEquals(SERIALIZED_BOOLEAN_FALSE, serializeReturnValue(boolean.class, false)); + } + + @Test + public void deserializeMethodParameters_booleanFalse() throws Exception { + expectDeserializedArgument(boolean.class, false, SERIALIZED_BOOLEAN_FALSE); + } + + // (byte) 42 + private static final byte[] SERIALIZED_BYTE = {0x00, 0x42, 42}; + + @Test + public void serializeReturnValue_byte() throws Exception { + assertArrayEquals(SERIALIZED_BYTE, serializeReturnValue(byte.class, (byte) 42)); + } + + @Test + public void deserializeMethodParameters_byte() throws Exception { + expectDeserializedArgument(byte.class, (byte) 42, SERIALIZED_BYTE); + } + + // '\u1122' + private static final byte[] SERIALIZED_CHAR = {0x00, 0x43, 0x11, 0x22}; + + @Test + public void serializeReturnValue_char() throws Exception { + assertArrayEquals(SERIALIZED_CHAR, serializeReturnValue(char.class, '\u1122')); + } + + @Test + public void deserializeMethodParameters_char() throws Exception { + expectDeserializedArgument(char.class, '\u1122', SERIALIZED_CHAR); + } + + // (short) 0x1011 + private static final byte[] SERIALIZED_SHORT = {0x00, 0x53, 0x10, 0x11}; + + @Test + public void serializeReturnValue_short() throws Exception { + assertArrayEquals(SERIALIZED_SHORT, + serializeReturnValue(short.class, (short) 0x1011)); + } + + @Test + public void deserializeMethodParameters_short() throws Exception { + expectDeserializedArgument(short.class, (short) 0x1011, SERIALIZED_SHORT); + } + + // 0x11223344 + private static final byte[] SERIALIZED_INT = {0x00, 0x49, 0x11, 0x22, 0x33, 0x44}; + + @Test + public void serializeReturnValue_int() throws Exception { + assertArrayEquals(SERIALIZED_INT, + serializeReturnValue(int.class, 0x11223344)); + } + + @Test + public void deserializeMethodParameters_int() throws Exception { + expectDeserializedArgument(int.class, 0x11223344, SERIALIZED_INT); + } + + // 0x0011223344556677L + private static final byte[] SERIALIZED_LONG = + {0x00, 0x4a, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77}; + + @Test + public void serializeReturnValue_long() throws Exception { + assertArrayEquals(SERIALIZED_LONG, + serializeReturnValue(long.class, 0x0011223344556677L)); + } + + @Test + public void deserializeMethodParameters_long() throws Exception { + expectDeserializedArgument(long.class, 0x0011223344556677L, SERIALIZED_LONG); + } + + // 3.141d + private static final byte[] SERIALIZED_DOUBLE = + {0x00, 0x44, (byte) 0x40, (byte) 0x09, (byte) 0x20, (byte) 0xc4, (byte) 0x9b, + (byte) 0xa5, (byte) 0xe3, (byte) 0x54}; + + @Test + public void serializeReturnValue_double() throws Exception { + assertArrayEquals( + SERIALIZED_DOUBLE, + serializeReturnValue(double.class, 3.141d)); + } + + @Test + public void deserializeMethodParameters_double() throws Exception { + expectDeserializedArgument(double.class, 3.141d, SERIALIZED_DOUBLE); + } + + // 3.141f + private static final byte[] SERIALIZED_FLOAT = + {0x00, 0x46, (byte) 0x40, (byte) 0x49, (byte) 0x06, (byte) 0x25}; + + @Test + public void serializeReturnValue_float() throws Exception { + assertArrayEquals(SERIALIZED_FLOAT, + serializeReturnValue(float.class, 3.141f)); + } + + @Test + public void deserializeMethodParameters_float() throws Exception { + expectDeserializedArgument(float.class, 3.141f, SERIALIZED_FLOAT); + } + + // "foo" + private static final byte[] SERIALIZED_ASCII_STRING = {0x00, 0x52, 0, 3, 0x66, 0x6f, 0x6f}; + + @Test + public void serializeReturnValue_asciiString() throws Exception { + assertArrayEquals(SERIALIZED_ASCII_STRING, + serializeReturnValue(String.class, "foo")); + } + + @Test + public void deserializeMethodParameters_asciiString() throws Exception { + expectDeserializedArgument(String.class, "foo", SERIALIZED_ASCII_STRING); + } + + // "\u1122" + private static final byte[] SERIALIZED_NON_ASCII_STRING = + {0x00, 0x52, 0, 3, (byte) 0xe1, (byte) 0x84, (byte) 0xa2}; + + @Test + public void serializeReturnValue_nonAsciiString_encodesAsUtf8() throws Exception { + assertArrayEquals(SERIALIZED_NON_ASCII_STRING, + serializeReturnValue(String.class, "\u1122")); + } + + @Test + public void deserializeMethodParameters_decodesFromUtf8() throws Exception { + expectDeserializedArgument(String.class, "\u1122", SERIALIZED_NON_ASCII_STRING); + } + + // "" + private static final byte[] SERIALIZED_EMPTY_STRING = {0x00, 0x52, 0, 0}; + + @Test + public void serializeReturnValue_emptyString() throws Exception { + assertArrayEquals(SERIALIZED_EMPTY_STRING, serializeReturnValue(String.class, "")); + } + + @Test + public void deserializeMethodParameters_emptyString() throws Exception { + expectDeserializedArgument(String.class, "", SERIALIZED_EMPTY_STRING); + } + + @Test + public void serializeReturnValue_nullString_encodesAsEmptyString() throws Exception { + assertArrayEquals(new byte[]{0x00, 0x52, 0, 0}, serializeReturnValue(String.class, null)); + } + + // Illegal - string length exceeding actual bytes + private static final byte[] SERIALIZED_INVALID_STRING = + {0x00, 0x52, 0, 3, 0x66}; + + @Test + public void deserializeMethodParameters_stringPayloadMissing_throws() throws Exception { + Object[] args = new Object[1]; + Class<?>[] argTypes = new Class<?>[1]; + assertThrows(BufferUnderflowException.class, + () -> deserializeMethodParameters(args, argTypes, + ByteBuffer.wrap(SERIALIZED_INVALID_STRING))); + } + + @Test + public void serializeAndDeserialize_handlesStringsUpTo64k() throws Exception { + char[] chars = new char[65535]; + Arrays.fill(chars, 'a'); + String original = new String(chars); + byte[] serialized = serializeReturnValue(String.class, original); + + // 2 bytes for the R signature char, 2 bytes char string byte count, 2^16-1 bytes ASCII + // payload + assertEquals(2 + 2 + 65535, serialized.length); + + // length is unsigned short + assertArrayEquals(new byte[]{0x00, 0x52, (byte) 0xff, (byte) 0xff}, + Arrays.copyOfRange(serialized, 0, 4)); + + // length of string must be interpreted as unsigned short, returning original content + expectDeserializedArgument(String.class, original, serialized); + } + + private static final byte[] SERIALIZED_VOID = {0x00, 0x56}; + + @Test + public void serializeReturnValue_void() throws Exception { + assertArrayEquals(SERIALIZED_VOID, serializeReturnValue(void.class, null)); + } + + @Test + public void deserializeMethodParameters_void_throws() throws Exception { + Object[] args = new Object[1]; + Class<?>[] argTypes = new Class<?>[1]; + assertThrows(ViewMethodInvocationSerializationException.class, + () -> deserializeMethodParameters(args, argTypes, + ByteBuffer.wrap(SERIALIZED_VOID))); + } + + // new byte[]{} + private static final byte[] SERIALIZED_EMPTY_BYTE_ARRAY = {0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 0}; + + @Test + public void serializeReturnValue_emptyByteArray() throws Exception { + assertArrayEquals(SERIALIZED_EMPTY_BYTE_ARRAY, + serializeReturnValue(byte[].class, new byte[]{})); + } + + @Test + public void deserializeMethodParameters_emptyByteArray() throws Exception { + expectDeserializedArgument(byte[].class, new byte[]{}, SERIALIZED_EMPTY_BYTE_ARRAY); + } + + // new byte[]{0, 42} + private static final byte[] SERIALIZED_SIMPLE_BYTE_ARRAY = + {0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 2, 0, 42}; + + @Test + public void serializeReturnValue_byteArray() throws Exception { + assertArrayEquals(SERIALIZED_SIMPLE_BYTE_ARRAY, + serializeReturnValue(byte[].class, new byte[]{0, 42})); + } + + @Test + public void deserializeMethodParameters_byteArray() throws Exception { + expectDeserializedArgument(byte[].class, new byte[]{0, 42}, SERIALIZED_SIMPLE_BYTE_ARRAY); + } + + @Test + public void serializeReturnValue_largeByteArray_encodesSizeCorrectly() throws Exception { + byte[] result = serializeReturnValue(byte[].class, new byte[0x012233]); + // 2 bytes for the each [Z signature char, 4 bytes int array length, 0x012233 bytes payload + assertEquals(2 + 2 + 4 + 74291, result.length); + + assertArrayEquals(new byte[]{0x00, 0x5b, 0x00, 0x42, 0x00, 0x01, 0x22, 0x33}, + Arrays.copyOfRange(result, 0, 8)); + } + + // Illegal - declared size exceeds remaining buffer length + private static final byte[] SERIALIZED_INVALID_BYTE_ARRAY = + {0x00, 0x5b, 0x00, 0x42, 0, 0, 0, 3, 0, 42}; + + @Test + public void deserializeMethodParameters_sizeExceedsBuffer_throws() throws Exception { + Object[] args = new Object[1]; + Class<?>[] argTypes = new Class<?>[1]; + assertThrows(BufferUnderflowException.class, + () -> deserializeMethodParameters(args, argTypes, + ByteBuffer.wrap(SERIALIZED_INVALID_BYTE_ARRAY))); + } + + // new int[]{} + private static final byte[] SERIALIZED_EMPTY_INT_ARRAY = {0x00, 0x5b, 0x00, 0x49, 0, 0, 0, 0}; + + @Test + public void serializeReturnValue_nonByteArrayType_throws() throws Exception { + assertThrows(ViewMethodInvocationSerializationException.class, + () -> serializeReturnValue(int[].class, 42)); + } + + @Test + public void deserializeMethodParameters_nonByteArrayType_throws() throws Exception { + Object[] args = new Object[1]; + Class<?>[] argTypes = new Class<?>[1]; + assertThrows(ViewMethodInvocationSerializationException.class, + () -> deserializeMethodParameters(args, argTypes, + ByteBuffer.wrap(SERIALIZED_EMPTY_INT_ARRAY))); + } + + // new byte[]{0, 42} + private static final byte[] SERIALIZED_MULTIPLE_PARAMETERS = + {0x00, 0x42, 42, 0x00, 0x5A, 1}; + + @Test + public void deserializeMethodParameters_multipleParameters() throws Exception { + expectDeserializedArguments(new Class[]{byte.class, boolean.class}, + new Object[]{(byte) 42, true}, SERIALIZED_MULTIPLE_PARAMETERS); + } + + // Illegal - type 'X' + private static final byte[] SERIALIZED_INVALID_UNKNOWN_TYPE = {0x00, 0x58}; + + @Test + public void deserializeMethodParameters_unknownType_throws() throws Exception { + Object[] args = new Object[1]; + Class<?>[] argTypes = new Class<?>[1]; + assertThrows(ViewMethodInvocationSerializationException.class, + () -> deserializeMethodParameters(args, argTypes, + ByteBuffer.wrap(SERIALIZED_INVALID_UNKNOWN_TYPE))); + } + + @Test + public void deserializeMethodParameters_noArgumentsEmptyPacket_isNoop() throws Exception { + Object[] args = new Object[0]; + Class<?>[] argTypes = new Class<?>[0]; + deserializeMethodParameters(args, argTypes, ByteBuffer.wrap(new byte[0])); + } + + @Test + public void deserializeMethodParameters_withArgumentsEmptyPacket_throws() throws Exception { + Object[] args = new Object[1]; + Class<?>[] argTypes = new Class<?>[1]; + assertThrows(BufferUnderflowException.class, + () -> deserializeMethodParameters(args, argTypes, ByteBuffer.wrap(new byte[0]))); + } + + private static void expectDeserializedArgument(Class<?> expectedType, Object expectedValue, + byte[] argumentBuffer) throws Exception { + expectDeserializedArguments(new Class[]{expectedType}, new Object[]{expectedValue}, + argumentBuffer); + } + + private static void expectDeserializedArguments(Class<?>[] expectedTypes, + Object[] expectedValues, byte[] argumentBuffer) throws Exception { + final int argCount = expectedTypes.length; + assertEquals("test helper not used correctly", argCount, expectedValues.length); + Object[] actualArgs = new Object[argCount]; + Class<?>[] actualArgTypes = new Class<?>[argCount]; + + ByteBuffer buffer = ByteBuffer.wrap(argumentBuffer); + deserializeMethodParameters(actualArgs, actualArgTypes, buffer); + + for (int i = 0; i < argCount; i++) { + String context = "argument " + i; + assertEquals(context, expectedTypes[i], actualArgTypes[i]); + if (byte[].class.equals(expectedTypes[i])) { + assertArrayEquals((byte[]) expectedValues[i], (byte[]) actualArgs[i]); + } else { + assertEquals(expectedValues[i], actualArgs[i]); + } + } + } +} |