Define a generic class to parse structured message.
Bug: 163492391
Test: atest android.net.util.StructTest --rerun-until-failure
Change-Id: I1848984d80998d87acccc52287a351d27708dedc
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index f4799f0..17372dc 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -48,7 +48,7 @@
"//frameworks/opt/net/ike",
"//frameworks/opt/net/wifi/service",
"//frameworks/opt/net/telephony",
- "//packages/modules/NetworkStack",
+ "//packages/modules/NetworkStack:__subpackages__",
"//packages/modules/CaptivePortalLogin",
"//frameworks/libs/net/common/tests:__subpackages__",
],
diff --git a/staticlibs/device/android/net/util/Struct.java b/staticlibs/device/android/net/util/Struct.java
new file mode 100644
index 0000000..d0c763e
--- /dev/null
+++ b/staticlibs/device/android/net/util/Struct.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2020 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.net.util;
+
+import android.annotation.NonNull;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.math.BigInteger;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Define a generic class that helps to parse the structured message.
+ *
+ * Example usage:
+ *
+ * // C-style NduserOption message header definition in the kernel:
+ * struct nduseroptmsg {
+ * unsigned char nduseropt_family;
+ * unsigned char nduseropt_pad1;
+ * unsigned short nduseropt_opts_len;
+ * int nduseropt_ifindex;
+ * __u8 nduseropt_icmp_type;
+ * __u8 nduseropt_icmp_code;
+ * unsigned short nduseropt_pad2;
+ * unsigned int nduseropt_pad3;
+ * }
+ *
+ * - Declare a subclass with explicit constructor or not which extends from this class to parse
+ * NduserOption header from raw bytes array.
+ *
+ * - Option w/ explicit constructor:
+ * static class NduserOptHeaderMessage extends Struct {
+ * @Field(order = 0, type = Type.U8, padding = 1)
+ * final short family;
+ * @Field(order = 1, type = Type.U16)
+ * final int len;
+ * @Field(order = 2, type = Type.S32)
+ * final int ifindex;
+ * @Field(order = 3, type = Type.U8)
+ * final short type;
+ * @Field(order = 4, type = Type.U8, padding = 6)
+ * final short code;
+ *
+ * NduserOptHeaderMessage(final short family, final int len, final int ifindex,
+ * final short type, final short code) {
+ * this.family = family;
+ * this.len = len;
+ * this.ifindex = ifindex;
+ * this.type = type;
+ * this.code = code;
+ * }
+ * }
+ *
+ * - Option w/o explicit constructor:
+ * static class NduserOptHeaderMessage extends Struct {
+ * @Field(order = 0, type = Type.U8, padding = 1)
+ * short family;
+ * @Field(order = 1, type = Type.U16)
+ * int len;
+ * @Field(order = 2, type = Type.S32)
+ * int ifindex;
+ * @Field(order = 3, type = Type.U8)
+ * short type;
+ * @Field(order = 4, type = Type.U8, padding = 6)
+ * short code;
+ * }
+ *
+ * - Parse the target message and refer the members.
+ * final ByteBuffer buf = ByteBuffer.wrap(RAW_BYTES_ARRAY);
+ * buf.order(ByteOrder.nativeOrder());
+ * final NduserOptHeaderMessage nduserHdrMsg = Struct.parse(NduserOptHeaderMessage.class, buf);
+ * assertEquals(10, nduserHdrMsg.family);
+ */
+public class Struct {
+ public enum Type {
+ U8, // unsigned byte, size = 1 byte
+ U16, // unsigned short, size = 2 bytes
+ U32, // unsigned int, size = 4 bytes
+ U64, // unsigned long, size = 8 bytes
+ S8, // signed byte, size = 1 byte
+ S16, // signed short, size = 2 bytes
+ S32, // signed int, size = 4 bytes
+ S64, // signed long, size = 8 bytes
+ BE16, // unsigned short in network order, size = 2 bytes
+ BE32, // unsigned int in network order, size = 4 bytes
+ BE64, // unsigned long in network order, size = 8 bytes
+ ByteArray, // byte array with predefined length
+ }
+
+ /**
+ * Indicate that the field marked with this annotation will automatically be managed by this
+ * class (e.g., will be parsed by #parse).
+ *
+ * order: The placeholder associated with each field, consecutive order starting from zero.
+ * type: The primitive data type listed in above Type enumeration.
+ * padding: Padding bytes appear after the field for alignment.
+ * arraysize: The length of byte array.
+ *
+ * Annotation associated with field MUST have order and type properties at least, padding
+ * and arraysize properties depend on the specific usage, if these properties are absence,
+ * then default value 0 will be applied.
+ */
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ public @interface Field {
+ int order();
+ Type type();
+ int padding() default 0;
+ int arraysize() default 0;
+ }
+
+ private static class FieldInfo {
+ @NonNull
+ public final Field annotation;
+ @NonNull
+ public final java.lang.reflect.Field field;
+
+ FieldInfo(final Field annotation, final java.lang.reflect.Field field) {
+ this.annotation = annotation;
+ this.field = field;
+ }
+ }
+
+ private static void checkAnnotationType(final Type type, final Class fieldType) {
+ switch (type) {
+ case U8:
+ case S16:
+ if (fieldType == Short.TYPE) return;
+ break;
+ case U16:
+ case S32:
+ case BE16:
+ if (fieldType == Integer.TYPE) return;
+ break;
+ case U32:
+ case S64:
+ case BE32:
+ if (fieldType == Long.TYPE) return;
+ break;
+ case U64:
+ case BE64:
+ if (fieldType == BigInteger.class || fieldType == Long.TYPE) return;
+ break;
+ case S8:
+ if (fieldType == Byte.TYPE) return;
+ break;
+ case ByteArray:
+ if (fieldType == byte[].class) return;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown type" + type);
+ }
+ throw new IllegalArgumentException("Invalid primitive data type: " + fieldType
+ + "for annotation type: " + type);
+ }
+
+ private static boolean isStructSubclass(final Class clazz) {
+ return clazz != null && Struct.class.isAssignableFrom(clazz) && Struct.class != clazz;
+ }
+
+ private static int getAnnotationFieldCount(final Class clazz) {
+ int count = 0;
+ for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
+ if (field.isAnnotationPresent(Field.class)) count++;
+ }
+ return count;
+ }
+
+ private static boolean matchModifier(final FieldInfo[] fields, boolean immutable) {
+ for (FieldInfo fi : fields) {
+ if (Modifier.isFinal(fi.field.getModifiers()) != immutable) return false;
+ }
+ return true;
+ }
+
+ private static boolean hasBothMutableAndImmutableFields(final FieldInfo[] fields) {
+ return !matchModifier(fields, true /* immutable */)
+ && !matchModifier(fields, false /* mutable */);
+ }
+
+ private static boolean matchConstructor(final Constructor cons, final FieldInfo[] fields) {
+ final Class[] paramTypes = cons.getParameterTypes();
+ if (paramTypes.length != fields.length) return false;
+ for (int i = 0; i < cons.getParameterTypes().length; i++) {
+ if (!paramTypes[i].equals(fields[i].field.getType())) return false;
+ }
+ return true;
+ }
+
+ private static BigInteger readBigInteger(final ByteBuffer buf) {
+ // The magnitude argument of BigInteger constructor is a byte array in big-endian order.
+ // If ByteBuffer is read in little-endian, reverse the order of the bytes is required;
+ // if ByteBuffer is read in big-endian, then just keep it as-is.
+ final byte[] input = new byte[8];
+ for (int i = 0; i < 8; i++) {
+ input[(buf.order() == ByteOrder.LITTLE_ENDIAN ? input.length - 1 - i : i)] = buf.get();
+ }
+ return new BigInteger(1, input);
+ }
+
+ private static Object getFieldValue(final ByteBuffer buf, final FieldInfo fieldInfo)
+ throws BufferUnderflowException {
+ final Object value;
+ checkAnnotationType(fieldInfo.annotation.type(), fieldInfo.field.getType());
+ switch (fieldInfo.annotation.type()) {
+ case U8:
+ value = (short) (buf.get() & 0xFF);
+ break;
+ case U16:
+ value = (int) (buf.getShort() & 0xFFFF);
+ break;
+ case U32:
+ value = (long) (buf.getInt() & 0xFFFFFFFFL);
+ break;
+ case U64:
+ if (fieldInfo.field.getType() == BigInteger.class) {
+ value = readBigInteger(buf);
+ } else {
+ value = buf.getLong();
+ }
+ break;
+ case S8:
+ value = buf.get();
+ break;
+ case S16:
+ value = buf.getShort();
+ break;
+ case S32:
+ value = buf.getInt();
+ break;
+ case S64:
+ value = buf.getLong();
+ break;
+ case BE16:
+ if (buf.order() == ByteOrder.LITTLE_ENDIAN) {
+ value = (int) (Short.reverseBytes(buf.getShort()) & 0xFFFF);
+ } else {
+ value = (int) (buf.getShort() & 0xFFFF);
+ }
+ break;
+ case BE32:
+ if (buf.order() == ByteOrder.LITTLE_ENDIAN) {
+ value = (long) (Integer.reverseBytes(buf.getInt()) & 0xFFFFFFFFL);
+ } else {
+ value = (long) (buf.getInt() & 0xFFFFFFFFL);
+ }
+ break;
+ case BE64:
+ if (fieldInfo.field.getType() == BigInteger.class) {
+ value = readBigInteger(buf);
+ } else {
+ if (buf.order() == ByteOrder.LITTLE_ENDIAN) {
+ value = Long.reverseBytes(buf.getLong());
+ } else {
+ value = buf.getLong();
+ }
+ }
+ break;
+ case ByteArray:
+ final byte[] array = new byte[fieldInfo.annotation.arraysize()];
+ buf.get(array);
+ value = array;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown type:" + fieldInfo.annotation.type());
+ }
+
+ // Skip the padding data for alignment if any.
+ if (fieldInfo.annotation.padding() > 0) {
+ buf.position(buf.position() + fieldInfo.annotation.padding());
+ }
+ return value;
+ }
+
+ private static FieldInfo[] getClassFieldInfo(final Class clazz) {
+ final FieldInfo[] annotationFields = new FieldInfo[getAnnotationFieldCount(clazz)];
+
+ // Since array returned from Class#getDeclaredFields doesn't guarantee the actual order
+ // of field appeared in the class, that is a problem when parsing raw data read from
+ // ByteBuffer. Store the fields appeared by the order() defined in the Field annotation.
+ for (java.lang.reflect.Field field : clazz.getDeclaredFields()) {
+ if (Modifier.isStatic(field.getModifiers())) continue;
+
+ final Field annotation = field.getAnnotation(Field.class);
+ if (annotation == null) {
+ throw new IllegalArgumentException("Field " + field.getName()
+ + " is missing the " + Field.class.getSimpleName()
+ + " annotation");
+ }
+ if (annotation.order() < 0 || annotation.order() >= annotationFields.length) {
+ throw new IllegalArgumentException("Annotation order: " + annotation.order()
+ + " is negative or non-consecutive");
+ }
+ if (annotationFields[annotation.order()] != null) {
+ throw new IllegalArgumentException("Duplicated annotation order: "
+ + annotation.order());
+ }
+ annotationFields[annotation.order()] = new FieldInfo(annotation, field);
+ }
+ return annotationFields;
+ }
+
+ /**
+ * Parse raw data from ByteBuffer according to the pre-defined annotation rule and return
+ * the type-variable object which is subclass of Struct class.
+ *
+ * TODO:
+ * 1. Support subclass inheritance.
+ * 2. Introduce annotation processor to enforce the subclass naming schema.
+ */
+ public static <T> T parse(final Class<T> clazz, final ByteBuffer buf) {
+ try {
+ if (!isStructSubclass(clazz)) {
+ throw new IllegalArgumentException(clazz.getName() + " is not a subclass of "
+ + Struct.class.getName());
+ }
+
+ final FieldInfo[] foundFields = getClassFieldInfo(clazz);
+ if (hasBothMutableAndImmutableFields(foundFields)) {
+ throw new IllegalArgumentException("Class has both immutable and mutable fields");
+ }
+
+ Constructor<?> constructor = null;
+ Constructor<?> defaultConstructor = null;
+ final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
+ for (Constructor cons : constructors) {
+ if (matchConstructor(cons, foundFields)) constructor = cons;
+ if (cons.getParameterTypes().length == 0) defaultConstructor = cons;
+ }
+
+ if (constructor == null && defaultConstructor == null) {
+ throw new IllegalArgumentException("Fail to find available constructor");
+ }
+ if (constructor != null) {
+ final Object[] args = new Object[constructor.getParameterTypes().length];
+ for (int i = 0; i < args.length; i++) {
+ args[i] = getFieldValue(buf, foundFields[i]);
+ }
+ return (T) constructor.newInstance(args);
+ }
+
+ final Object instance = defaultConstructor.newInstance();
+ for (FieldInfo fi : foundFields) {
+ fi.field.set(instance, getFieldValue(buf, fi));
+ }
+ return (T) instance;
+ } catch (IllegalAccessException
+ | InvocationTargetException
+ | InstantiationException e) {
+ throw new IllegalArgumentException("Fail to create a instance from constructor", e);
+ } catch (BufferUnderflowException e) {
+ throw new IllegalArgumentException("Fail to read raw data from ByteBuffer", e);
+ }
+ }
+}
diff --git a/staticlibs/tests/unit/jarjar-rules.txt b/staticlibs/tests/unit/jarjar-rules.txt
index fceccfb..0f131b2 100644
--- a/staticlibs/tests/unit/jarjar-rules.txt
+++ b/staticlibs/tests/unit/jarjar-rules.txt
@@ -1,6 +1,7 @@
# TODO: move the classes to the target package in java
-rule android.net.util.IpRange* com.android.net.module.util.IpRange@1
-rule android.net.util.MacAddressUtils* com.android.net.module.util.MacAddressUtils@1
-rule android.net.util.LinkPropertiesUtils* com.android.net.module.util.LinkPropertiesUtils@1
-rule android.net.util.NetUtils* com.android.net.module.util.NetUtils@1
-rule android.net.util.nsd.** com.android.net.module.util.nsd.@1
+rule android.net.util.IpRange* com.android.net.moduletests.util.IpRange@1
+rule android.net.util.MacAddressUtils* com.android.net.moduletests.util.MacAddressUtils@1
+rule android.net.util.LinkPropertiesUtils* com.android.net.moduletests.util.LinkPropertiesUtils@1
+rule android.net.util.NetUtils* com.android.net.moduletests.util.NetUtils@1
+rule android.net.util.nsd.** com.android.net.moduletests.util.nsd.@1
+rule com.android.net.module.util.** com.android.net.moduletests.util.@1
diff --git a/staticlibs/tests/unit/src/android/net/util/StructTest.java b/staticlibs/tests/unit/src/android/net/util/StructTest.java
new file mode 100644
index 0000000..2d35891
--- /dev/null
+++ b/staticlibs/tests/unit/src/android/net/util/StructTest.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2020 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.net.util;
+
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.IpPrefix;
+import android.net.util.Struct.Field;
+import android.net.util.Struct.Type;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.HexDump;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructTest {
+
+ // IPv6, 0 bytes of options, ifindex 15715755: 0x00efcdab, type 134 (RA), code 0, padding.
+ private static final String HDR_EMPTY = "0a00" + "0000" + "abcdef00" + "8600000000000000";
+
+ // BE16: 0xfeff, BE32: 0xfeffffff, BE64: 0xfffffffffffffffe, 0x7effffffffffffff
+ private static final String NETWORK_ORDER_MSG = "feff" + "feffffff" + "fffffffffffffffe"
+ + "7effffffffffffff";
+
+ // S8: 0x7f, S16: 0x7fff, S32: 0x7fffffff, S64: 0x7fffffffffffffff
+ private static final String SIGNED_DATA = "7f" + "ff7f" + "ffffff7f" + "ffffffffffffff7f";
+
+ // nS8: 0x81, nS16: 0x8001, nS32: 0x80000001, nS64: 800000000000000001
+ private static final String SIGNED_NEGATIVE_DATA = "81" + "0180" + "01000080"
+ + "0100000000000080";
+
+ // U8: 0xff, U16: 0xffff, U32: 0xffffffff, U64: 0xffffffffffffffff, U63: 0x7fffffffffffffff;
+ private static final String UNSIGNED_DATA = "ff" + "ffff" + "ffffffff" + "ffffffffffffffff"
+ + "ffffffffffffff7f";
+
+ // PREF64 option, 2001:db8:3:4:5:6::/96, lifetime: 10064
+ private static final String OPT_PREF64 = "2750" + "20010db80003000400050006";
+
+ private <T> T doParsingMessageTest(final String hexString, Class<T> clazz) {
+ final ByteBuffer buf = toByteBuffer(hexString);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ return Struct.parse(clazz, buf);
+ }
+
+ static class HeaderMsgWithConstructor extends Struct {
+ static int sType;
+ static int sLength;
+
+ @Field(order = 0, type = Type.U8, padding = 1)
+ final short mFamily;
+ @Field(order = 1, type = Type.U16)
+ final int mLen;
+ @Field(order = 2, type = Type.S32)
+ final int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ final short mIcmpType;
+ @Field(order = 4, type = Type.U8, padding = 6)
+ final short mIcmpCode;
+
+ HeaderMsgWithConstructor(final short family, final int len, final int ifindex,
+ final short type, final short code) {
+ mFamily = family;
+ mLen = len;
+ mIfindex = ifindex;
+ mIcmpType = type;
+ mIcmpCode = code;
+ }
+ }
+
+ @Test
+ public void testClassWithExplicitConstructor() {
+ final HeaderMsgWithConstructor msg = doParsingMessageTest(HDR_EMPTY,
+ HeaderMsgWithConstructor.class);
+ assertEquals(10, msg.mFamily);
+ assertEquals(0, msg.mLen);
+ assertEquals(15715755, msg.mIfindex);
+ assertEquals(134, msg.mIcmpType);
+ assertEquals(0, msg.mIcmpCode);
+ }
+
+ static class HeaderMsgWithoutConstructor extends Struct {
+ static int sType;
+ static int sLength;
+
+ @Field(order = 0, type = Type.U8, padding = 1)
+ short mFamily;
+ @Field(order = 1, type = Type.U16)
+ int mLen;
+ @Field(order = 2, type = Type.S32)
+ int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ short mIcmpType;
+ @Field(order = 4, type = Type.U8, padding = 6)
+ short mIcmpCode;
+ }
+
+ @Test
+ public void testClassWithDefaultConstrcutor() {
+ final HeaderMsgWithoutConstructor msg = doParsingMessageTest(HDR_EMPTY,
+ HeaderMsgWithoutConstructor.class);
+ assertEquals(10, msg.mFamily);
+ assertEquals(0, msg.mLen);
+ assertEquals(15715755, msg.mIfindex);
+ assertEquals(134, msg.mIcmpType);
+ assertEquals(0, msg.mIcmpCode);
+ }
+
+ static class HeaderMessage {
+ @Field(order = 0, type = Type.U8, padding = 1)
+ short mFamily;
+ @Field(order = 1, type = Type.U16)
+ int mLen;
+ @Field(order = 2, type = Type.S32)
+ int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ short mIcmpType;
+ @Field(order = 4, type = Type.U8, padding = 6)
+ short mIcmpCode;
+ }
+
+ @Test
+ public void testInvalidClass_NotSubClass() {
+ final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(HeaderMessage.class, buf));
+ }
+
+ static class HeaderMessageMissingAnnotation extends Struct {
+ @Field(order = 0, type = Type.U8, padding = 1)
+ short mFamily;
+ @Field(order = 1, type = Type.U16)
+ int mLen;
+ int mIfindex;
+ @Field(order = 2, type = Type.U8)
+ short mIcmpType;
+ @Field(order = 3, type = Type.U8, padding = 6)
+ short mIcmpCode;
+ }
+
+ @Test
+ public void testInvalidClass_MissingAnnotationField() {
+ final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(HeaderMessageMissingAnnotation.class, buf));
+ }
+
+ static class NetworkOrderMessage extends Struct {
+ @Field(order = 0, type = Type.BE16)
+ final int mBE16;
+ @Field(order = 1, type = Type.BE32)
+ final long mBE32;
+ @Field(order = 2, type = Type.BE64)
+ final BigInteger mBE64;
+ @Field(order = 3, type = Type.BE64)
+ final long mBE63;
+
+ NetworkOrderMessage(final int be16, final long be32, final BigInteger be64,
+ final long be63) {
+ mBE16 = be16;
+ mBE32 = be32;
+ mBE64 = be64;
+ mBE63 = be63;
+ }
+ }
+
+ @Test
+ public void testNetworkOrder() {
+ final NetworkOrderMessage msg = doParsingMessageTest(NETWORK_ORDER_MSG,
+ NetworkOrderMessage.class);
+ assertEquals(65279, msg.mBE16);
+ assertEquals(4278190079L, msg.mBE32);
+ assertEquals(new BigInteger("18374686479671623679"), msg.mBE64);
+ assertEquals(9151314442816847871L, msg.mBE63);
+ }
+
+ static class UnsignedDataMessage extends Struct {
+ @Field(order = 0, type = Type.U8)
+ final short mU8;
+ @Field(order = 1, type = Type.U16)
+ final int mU16;
+ @Field(order = 2, type = Type.U32)
+ final long mU32;
+ @Field(order = 3, type = Type.U64)
+ final BigInteger mU64;
+ @Field(order = 4, type = Type.U64)
+ final long mU63;
+
+ UnsignedDataMessage(final short u8, final int u16, final long u32, final BigInteger u64,
+ final long u63) {
+ mU8 = u8;
+ mU16 = u16;
+ mU32 = u32;
+ mU64 = u64;
+ mU63 = u63;
+ }
+ }
+
+ @Test
+ public void testUnsignedData() {
+ final UnsignedDataMessage msg = doParsingMessageTest(UNSIGNED_DATA,
+ UnsignedDataMessage.class);
+ assertEquals(255, msg.mU8);
+ assertEquals(65535, msg.mU16);
+ assertEquals(4294967295L, msg.mU32);
+ assertEquals(new BigInteger("18446744073709551615"), msg.mU64);
+ assertEquals(9223372036854775807L, msg.mU63);
+ }
+
+ static class SignedDataMessage extends Struct {
+ @Field(order = 0, type = Type.S8)
+ final byte mS8;
+ @Field(order = 1, type = Type.S16)
+ final short mS16;
+ @Field(order = 2, type = Type.S32)
+ final int mS32;
+ @Field(order = 3, type = Type.S64)
+ final long mS64;
+ @Field(order = 4, type = Type.S8)
+ final byte mNS8;
+ @Field(order = 5, type = Type.S16)
+ final short mNS16;
+ @Field(order = 6, type = Type.S32)
+ final int mNS32;
+ @Field(order = 7, type = Type.S64)
+ final long mNS64;
+
+ SignedDataMessage(final byte s8, final short s16, final int s32, final long s64,
+ final byte nS8, final short nS16, final int nS32, final long nS64) {
+ mS8 = s8;
+ mS16 = s16;
+ mS32 = s32;
+ mS64 = s64;
+ mNS8 = nS8;
+ mNS16 = nS16;
+ mNS32 = nS32;
+ mNS64 = nS64;
+ }
+ }
+
+ @Test
+ public void testSignedData() {
+ final SignedDataMessage msg = doParsingMessageTest(SIGNED_DATA + SIGNED_NEGATIVE_DATA,
+ SignedDataMessage.class);
+ assertEquals(127, msg.mS8);
+ assertEquals(32767, msg.mS16);
+ assertEquals(2147483647, msg.mS32);
+ assertEquals(9223372036854775807L, msg.mS64);
+
+ assertEquals(-127, msg.mNS8);
+ assertEquals(-32767, msg.mNS16);
+ assertEquals(-2147483647, msg.mNS32);
+ assertEquals(-9223372036854775807L, msg.mNS64);
+ }
+
+ static class HeaderMessageWithDuplicateOrder extends Struct {
+ @Field(order = 0, type = Type.U8, padding = 1)
+ short mFamily;
+ @Field(order = 1, type = Type.U16)
+ int mLen;
+ @Field(order = 2, type = Type.S32)
+ int mIfindex;
+ @Field(order = 2, type = Type.U8)
+ short mIcmpType;
+ @Field(order = 3, type = Type.U8, padding = 6)
+ short mIcmpCode;
+ }
+
+ @Test
+ public void testInvalidClass_DuplicateFieldOrder() {
+ final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(HeaderMessageWithDuplicateOrder.class, buf));
+ }
+
+ static class HeaderMessageWithNegativeOrder extends Struct {
+ @Field(order = 0, type = Type.U8, padding = 1)
+ short mFamily;
+ @Field(order = 1, type = Type.U16)
+ int mLen;
+ @Field(order = 2, type = Type.S32)
+ int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ short mIcmpType;
+ @Field(order = -4, type = Type.U8, padding = 6)
+ short mIcmpCode;
+ }
+
+ @Test
+ public void testInvalidClass_NegativeFieldOrder() {
+ final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(HeaderMessageWithNegativeOrder.class, buf));
+ }
+
+ static class HeaderMessageOutOfIndexBounds extends Struct {
+ @Field(order = 0, type = Type.U8, padding = 1)
+ short mFamily;
+ @Field(order = 1, type = Type.U16)
+ int mLen;
+ @Field(order = 2, type = Type.S32)
+ int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ short mIcmpType;
+ @Field(order = 5, type = Type.U8, padding = 6)
+ short mIcmpCode;
+ }
+
+ @Test
+ public void testInvalidClass_OutOfIndexBounds() {
+ final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(HeaderMessageOutOfIndexBounds.class, buf));
+ }
+
+ static class HeaderMessageMismatchedPrimitiveType extends Struct {
+ @Field(order = 0, type = Type.U8, padding = 1)
+ short mFamily;
+ @Field(order = 1, type = Type.U16)
+ short mLen; // should be integer
+ @Field(order = 2, type = Type.S32)
+ int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ short mIcmpType;
+ @Field(order = 4, type = Type.U8, padding = 6)
+ short mIcmpCode;
+ }
+
+ @Test
+ public void testInvalidClass_MismatchedPrimitiveDataType() {
+ final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(HeaderMessageMismatchedPrimitiveType.class, buf));
+ }
+
+ static class PrefixMessage extends Struct {
+ @Field(order = 0, type = Type.BE16)
+ final int mLifetime;
+ @Field(order = 1, type = Type.ByteArray, arraysize = 12)
+ final byte[] mPrefix;
+
+ PrefixMessage(final int lifetime, final byte[] prefix) {
+ mLifetime = lifetime;
+ mPrefix = prefix;
+ }
+ }
+
+ @Test
+ public void testPrefixArrayField() throws Exception {
+ final ByteBuffer buf = toByteBuffer(OPT_PREF64);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+
+ // The original PREF64 option message has just 12 bytes for prefix byte array
+ // (Highest 96 bits of the Prefix), copyOf pads the 128-bits IPv6 address with
+ // prefix and 4-bytes zeros.
+ final PrefixMessage msg = Struct.parse(PrefixMessage.class, buf);
+ final InetAddress addr = InetAddress.getByAddress(Arrays.copyOf(msg.mPrefix, 16));
+ final IpPrefix prefix = new IpPrefix(addr, 96);
+ assertEquals(10064, msg.mLifetime);
+ assertTrue(prefix.equals(new IpPrefix("2001:db8:3:4:5:6::/96")));
+ }
+
+ static class HeaderMessageWithMutableField extends Struct {
+ @Field(order = 0, type = Type.U8, padding = 1)
+ final short mFamily;
+ @Field(order = 1, type = Type.U16)
+ final int mLen;
+ @Field(order = 2, type = Type.S32)
+ int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ short mIcmpType;
+ @Field(order = 4, type = Type.U8, padding = 6)
+ final short mIcmpCode;
+
+ HeaderMessageWithMutableField(final short family, final int len, final short code) {
+ mFamily = family;
+ mLen = len;
+ mIcmpCode = code;
+ }
+ }
+
+ @Test
+ public void testMixMutableAndImmutableFields() {
+ final ByteBuffer buf = toByteBuffer(HDR_EMPTY);
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(HeaderMessageWithMutableField.class, buf));
+ }
+
+ static class HeaderMsgWithStaticConstant extends Struct {
+ private static final String TAG = "HeaderMessage";
+ private static final int FIELD_COUNT = 5;
+
+ @Field(order = 0, type = Type.U8, padding = 1)
+ final short mFamily;
+ @Field(order = 1, type = Type.U16)
+ final int mLen;
+ @Field(order = 2, type = Type.S32)
+ final int mIfindex;
+ @Field(order = 3, type = Type.U8)
+ final short mIcmpType;
+ @Field(order = 4, type = Type.U8, padding = 6)
+ final short mIcmpCode;
+
+ HeaderMsgWithStaticConstant(final short family, final int len, final int ifindex,
+ final short type, final short code) {
+ mFamily = family;
+ mLen = len;
+ mIfindex = ifindex;
+ mIcmpType = type;
+ mIcmpCode = code;
+ }
+ }
+
+ @Test
+ public void testStaticConstantField() {
+ final HeaderMsgWithStaticConstant msg = doParsingMessageTest(HDR_EMPTY,
+ HeaderMsgWithStaticConstant.class);
+ assertEquals(10, msg.mFamily);
+ assertEquals(0, msg.mLen);
+ assertEquals(15715755, msg.mIfindex);
+ assertEquals(134, msg.mIcmpType);
+ assertEquals(0, msg.mIcmpCode);
+ }
+
+ static class MismatchedConstructor extends Struct {
+ @Field(order = 0, type = Type.U16) final int mInt1;
+ @Field(order = 1, type = Type.U16) final int mInt2;
+ MismatchedConstructor(String int1, String int2) {
+ mInt1 = Integer.valueOf(int1);
+ mInt2 = Integer.valueOf(int2);
+ }
+ }
+
+ @Test
+ public void testMisMatchedConstructor() {
+ final ByteBuffer buf = toByteBuffer("1234" + "5678");
+ assertThrows(IllegalArgumentException.class,
+ () -> Struct.parse(MismatchedConstructor.class, buf));
+ }
+
+ static class ClassWithTwoConstructors extends Struct {
+ @Field(order = 0, type = Type.U16) final int mInt1;
+ @Field(order = 1, type = Type.U16) final int mInt2;
+ ClassWithTwoConstructors(String int1, String int2) {
+ mInt1 = Integer.valueOf(int1);
+ mInt2 = Integer.valueOf(int2);
+ }
+ ClassWithTwoConstructors(int int1, int int2) {
+ mInt1 = int1;
+ mInt2 = int2;
+ }
+ }
+
+ @Test
+ public void testClassWithTwoConstructors() {
+ final ClassWithTwoConstructors msg = doParsingMessageTest("1234" + "5678",
+ ClassWithTwoConstructors.class);
+ assertEquals(13330 /* 0x3412 */, msg.mInt1);
+ assertEquals(30806 /* 0x7856 */, msg.mInt2);
+ }
+
+ private ByteBuffer toByteBuffer(final String hexString) {
+ return ByteBuffer.wrap(HexDump.hexStringToByteArray(hexString));
+ }
+}