Implement generic toString/equals/hashCode in Struct.
Bug: 163492391
Test: atest NetworkStaticLibTests
Change-Id: I485e0bbc6949c1eb239d52401ff0403dd447619b
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index 07a7e7a..afc2e64 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -30,6 +30,8 @@
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -111,7 +113,7 @@
UBE63, // unsigned long(MSB: 0) in network order, size = 8 bytes
UBE64, // unsigned long in network order, size = 8 bytes
ByteArray, // byte array with predefined length
- EUI48, // a 48-bits long MAC address
+ EUI48, // IEEE Extended Unique Identifier, a 48-bits long MAC address in network order
}
/**
@@ -607,4 +609,75 @@
public final byte[] writeToBytes() {
return writeToBytes(ByteOrder.nativeOrder());
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null || this.getClass() != obj.getClass()) return false;
+
+ final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+ for (int i = 0; i < fieldInfos.length; i++) {
+ try {
+ final Object value = fieldInfos[i].field.get(this);
+ final Object otherValue = fieldInfos[i].field.get(obj);
+
+ // Use Objects#deepEquals because the equals method on arrays does not check the
+ // contents of the array. The only difference between Objects#deepEquals and
+ // Objects#equals is that the former will call Arrays#deepEquals when comparing
+ // arrays. In turn, the only difference between Arrays#deepEquals is that it
+ // supports nested arrays. Struct does not currently support these, and if it did,
+ // Objects#deepEquals might be more correct.
+ if (!Objects.deepEquals(value, otherValue)) return false;
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Cannot access field: " + fieldInfos[i].field, e);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+ final Object[] values = new Object[fieldInfos.length];
+ for (int i = 0; i < fieldInfos.length; i++) {
+ final Object value;
+ try {
+ value = fieldInfos[i].field.get(this);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Cannot access field: " + fieldInfos[i].field, e);
+ }
+ // For byte array field, put the hash code generated based on the array content into
+ // the Object array instead of the reference to byte array, which might change and cause
+ // to get a different hash code even with the exact same elements.
+ if (fieldInfos[i].field.getType() == byte[].class) {
+ values[i] = Arrays.hashCode((byte[]) value);
+ } else {
+ values[i] = value;
+ }
+ }
+ return Objects.hash(values);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ final FieldInfo[] fieldInfos = getClassFieldInfo(this.getClass());
+ for (int i = 0; i < fieldInfos.length; i++) {
+ sb.append(fieldInfos[i].field.getName()).append(": ");
+ final Object value;
+ try {
+ value = fieldInfos[i].field.get(this);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Cannot access field: " + fieldInfos[i].field, e);
+ }
+ if (value == null) {
+ sb.append("null");
+ } else if (fieldInfos[i].field.getType() == byte[].class) {
+ sb.append("0x").append(HexDump.toHexString((byte[]) value));
+ } else {
+ sb.append(value.toString());
+ }
+ if (i != fieldInfos.length - 1) sb.append(", ");
+ }
+ return sb.toString();
+ }
}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index d5e27fb..253b911 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -20,6 +20,8 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import android.net.IpPrefix;
@@ -67,6 +69,10 @@
// PREF64 option, 2001:db8:3:4:5:6::/96, lifetime: 10064
private static final String OPT_PREF64 = "2750" + "20010db80003000400050006";
+ private static final byte[] TEST_PREFIX64 = new byte[]{
+ (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, (byte) 0x00, (byte) 0x03,
+ (byte) 0x00, (byte) 0x04, (byte) 0x00, (byte) 0x05, (byte) 0x00, (byte) 0x06,
+ };
private <T> T doParsingMessageTest(final String hexString, final Class<T> clazz,
final ByteOrder order) {
@@ -699,7 +705,7 @@
}
@Test
- public void testStructToByteArray() {
+ public void testStructToByteArrayRoundTrip() {
final SignedDataMessage littleEndianMsg = doParsingMessageTest(SIGNED_DATA,
SignedDataMessage.class, ByteOrder.LITTLE_ENDIAN);
assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
@@ -714,6 +720,229 @@
ByteOrder.LITTLE_ENDIAN) ? littleEndianMsg : bigEndianMsg;
assertArrayEquals(toByteBuffer(SIGNED_DATA).array(),
nativeOrderMsg.writeToBytes());
+ }
+ @Test
+ public void testStructToByteArray() {
+ final SignedDataMessage msg = new SignedDataMessage((byte) -5, (short) 42, (int) 0xff000004,
+ (long) 0xff000004ff000005L);
+ final String leHexString = "fb" + "2a00" + "040000ff" + "050000ff040000ff";
+ final String beHexString = "fb" + "002a" + "ff000004" + "ff000004ff000005";
+ final String hexString = ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)
+ ? leHexString : beHexString;
+ assertArrayEquals(toByteBuffer(hexString).array(), msg.writeToBytes());
+ }
+
+ static class FullTypeMessage extends Struct {
+ @Field(order = 0, type = Type.U8) public final short u8;
+ @Field(order = 1, type = Type.U16) public final int u16;
+ @Field(order = 2, type = Type.U32) public final long u32;
+ @Field(order = 3, type = Type.U63) public final long u63;
+ @Field(order = 4, type = Type.U64) public final BigInteger u64;
+ @Field(order = 5, type = Type.S8) public final byte s8;
+ @Field(order = 6, type = Type.S16) public final short s16;
+ @Field(order = 7, type = Type.S32) public final int s32;
+ @Field(order = 8, type = Type.S64) public final long s64;
+ @Field(order = 9, type = Type.UBE16) public final int ube16;
+ @Field(order = 10, type = Type.UBE32) public final long ube32;
+ @Field(order = 11, type = Type.UBE63) public final long ube63;
+ @Field(order = 12, type = Type.UBE64) public final BigInteger ube64;
+ @Field(order = 13, type = Type.ByteArray, arraysize = 12) public final byte[] bytes;
+ @Field(order = 14, type = Type.EUI48) public final MacAddress eui48;
+
+ FullTypeMessage(final short u8, final int u16, final long u32, final long u63,
+ final BigInteger u64, final byte s8, final short s16, final int s32, final long s64,
+ final int ube16, final long ube32, final long ube63, final BigInteger ube64,
+ final byte[] bytes, final MacAddress eui48) {
+ this.u8 = u8;
+ this.u16 = u16;
+ this.u32 = u32;
+ this.u63 = u63;
+ this.u64 = u64;
+ this.s8 = s8;
+ this.s16 = s16;
+ this.s32 = s32;
+ this.s64 = s64;
+ this.ube16 = ube16;
+ this.ube32 = ube32;
+ this.ube63 = ube63;
+ this.ube64 = ube64;
+ this.bytes = bytes;
+ this.eui48 = eui48;
+ }
+ }
+
+ private static final String FULL_TYPE_DATA = "ff" + "ffff" + "ffffffff" + "7fffffffffffffff"
+ + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff" + "7fffffffffffffff" + "7fff"
+ + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff" + "20010db80003000400050006"
+ + "001122334455";
+ private static final String FULL_TYPE_DATA_DIFF_MAC = "ff" + "ffff" + "ffffffff"
+ + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+ + "7fffffffffffffff" + "7fff" + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff"
+ + "20010db80003000400050006" + "112233445566";
+ private static final String FULL_TYPE_DATA_DIFF_LONG = "ff" + "ffff" + "ffffffff"
+ + "7ffffffffffffffe" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+ + "7fffffffffffffff" + "7fff" + "7fffffff" + "7fffffffffffffff" + "ffffffffffffffff"
+ + "20010db80003000400050006" + "001122334455";
+ private static final String FULL_TYPE_DATA_DIFF_INTEGER = "ff" + "ffff" + "ffffffff"
+ + "7fffffffffffffff" + "ffffffffffffffff" + "7f" + "7fff" + "7fffffff"
+ + "7fffffffffffffff" + "7fff" + "ffffff7f" + "7fffffffffffffff" + "ffffffffffffffff"
+ + "20010db80003000400050006" + "001122334455";
+ @Test
+ public void testStructClass_equals() {
+ final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+ ByteOrder.BIG_ENDIAN);
+
+ assertEquals(255, msg.u8);
+ assertEquals(65535, msg.u16);
+ assertEquals(4294967295L, msg.u32);
+ assertEquals(9223372036854775807L, msg.u63);
+ assertEquals(new BigInteger("18446744073709551615"), msg.u64);
+ assertEquals(127, msg.s8);
+ assertEquals(32767, msg.s16);
+ assertEquals(2147483647, msg.s32);
+ assertEquals(9223372036854775807L, msg.s64);
+ assertEquals(32767, msg.ube16);
+ assertEquals(2147483647, msg.ube32);
+ assertEquals(9223372036854775807L, msg.ube63);
+ assertEquals(new BigInteger("18446744073709551615"), msg.ube64);
+ assertArrayEquals(TEST_PREFIX64, msg.bytes);
+ assertEquals(MacAddress.fromString("00:11:22:33:44:55"), msg.eui48);
+
+ assertEquals(78, msg.getSize(FullTypeMessage.class));
+ assertArrayEquals(toByteBuffer(FULL_TYPE_DATA).array(),
+ msg.writeToBytes(ByteOrder.BIG_ENDIAN));
+
+ final FullTypeMessage msg1 = new FullTypeMessage((short) 0xff, (int) 0xffff,
+ (long) 0xffffffffL, (long) 0x7fffffffffffffffL,
+ new BigInteger("18446744073709551615"), (byte) 0x7f, (short) 0x7fff,
+ (int) 0x7fffffff, (long) 0x7fffffffffffffffL, (int) 0x7fff, (long) 0x7fffffffL,
+ (long) 0x7fffffffffffffffL, new BigInteger("18446744073709551615"), TEST_PREFIX64,
+ MacAddress.fromString("00:11:22:33:44:55"));
+ assertTrue(msg.equals(msg1));
+ }
+
+ static class FullTypeMessageWithDupType extends Struct {
+ @Field(order = 0, type = Type.U8) public final short u8;
+ @Field(order = 1, type = Type.U16) public final int u16;
+ @Field(order = 2, type = Type.U32) public final long u32;
+ @Field(order = 3, type = Type.S64) public final long u63; // old: U63, new: S64
+ @Field(order = 4, type = Type.UBE64) public final BigInteger u64; // old: U64, new: UBE64
+ @Field(order = 5, type = Type.S8) public final byte s8;
+ @Field(order = 6, type = Type.S16) public final short s16;
+ @Field(order = 7, type = Type.S32) public final int s32;
+ @Field(order = 8, type = Type.S64) public final long s64;
+ @Field(order = 9, type = Type.U16) public final int ube16; // old:UBE16, new: U16
+ @Field(order = 10, type = Type.UBE32) public final long ube32;
+ @Field(order = 11, type = Type.UBE63) public final long ube63;
+ @Field(order = 12, type = Type.UBE64) public final BigInteger ube64;
+ @Field(order = 13, type = Type.ByteArray, arraysize = 12) public final byte[] bytes;
+ @Field(order = 14, type = Type.EUI48) public final MacAddress eui48;
+
+ FullTypeMessageWithDupType(final short u8, final int u16, final long u32, final long u63,
+ final BigInteger u64, final byte s8, final short s16, final int s32, final long s64,
+ final int ube16, final long ube32, final long ube63, final BigInteger ube64,
+ final byte[] bytes, final MacAddress eui48) {
+ this.u8 = u8;
+ this.u16 = u16;
+ this.u32 = u32;
+ this.u63 = u63;
+ this.u64 = u64;
+ this.s8 = s8;
+ this.s16 = s16;
+ this.s32 = s32;
+ this.s64 = s64;
+ this.ube16 = ube16;
+ this.ube32 = ube32;
+ this.ube63 = ube63;
+ this.ube64 = ube64;
+ this.bytes = bytes;
+ this.eui48 = eui48;
+ }
+ }
+
+ @Test
+ public void testStructClass_notEqualWithDifferentClass() {
+ final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+ ByteOrder.BIG_ENDIAN);
+ final FullTypeMessageWithDupType msg1 = doParsingMessageTest(FULL_TYPE_DATA,
+ FullTypeMessageWithDupType.class, ByteOrder.BIG_ENDIAN);
+
+ assertFalse(msg.equals(msg1));
+ }
+
+ @Test
+ public void testStructClass_notEqualWithDifferentValue() {
+ final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+ ByteOrder.BIG_ENDIAN);
+
+ // With different MAC address.
+ final FullTypeMessage msg1 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_MAC,
+ FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+ assertNotEquals(msg.eui48, msg1.eui48);
+ assertFalse(msg.equals(msg1));
+
+ // With different byte array.
+ final FullTypeMessage msg2 = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+ ByteOrder.BIG_ENDIAN);
+ msg2.bytes[5] = (byte) 42; // change one byte in the array.
+ assertFalse(msg.equals(msg2));
+
+ // With different Long primitive.
+ final FullTypeMessage msg3 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_LONG,
+ FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+ assertNotEquals(msg.u63, msg3.u63);
+ assertFalse(msg.equals(msg3));
+
+ // With different Integer primitive.
+ final FullTypeMessage msg4 = doParsingMessageTest(FULL_TYPE_DATA_DIFF_INTEGER,
+ FullTypeMessage.class, ByteOrder.BIG_ENDIAN);
+ assertNotEquals(msg.ube32, msg4.ube32);
+ assertFalse(msg.equals(msg4));
+
+ }
+
+ @Test
+ public void testStructClass_toString() {
+ final String expected = "u8: 255, u16: 65535, u32: 4294967295,"
+ + " u63: 9223372036854775807, u64: 18446744073709551615, s8: 127, s16: 32767,"
+ + " s32: 2147483647, s64: 9223372036854775807, ube16: 32767, ube32: 2147483647,"
+ + " ube63: 9223372036854775807, ube64: 18446744073709551615,"
+ + " bytes: 0x20010DB80003000400050006,"
+ + " eui48: 00:11:22:33:44:55";
+
+ final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+ ByteOrder.BIG_ENDIAN);
+ assertEquals(expected, msg.toString());
+ }
+
+ @Test
+ public void testStructClass_toStringWithNullMember() {
+ final String expected = "u8: 255, u16: 65535, u32: 4294967295,"
+ + " u63: 9223372036854775807, u64: null, s8: 127, s16: 32767,"
+ + " s32: 2147483647, s64: 9223372036854775807, ube16: 32767, ube32: 2147483647,"
+ + " ube63: 9223372036854775807, ube64: 18446744073709551615,"
+ + " bytes: null, eui48: null";
+
+ final FullTypeMessage msg = new FullTypeMessage((short) 0xff, (int) 0xffff,
+ (long) 0xffffffffL, (long) 0x7fffffffffffffffL,
+ null /* u64 */, (byte) 0x7f, (short) 0x7fff,
+ (int) 0x7fffffff, (long) 0x7fffffffffffffffL, (int) 0x7fff, (long) 0x7fffffffL,
+ (long) 0x7fffffffffffffffL, new BigInteger("18446744073709551615"),
+ null /* bytes */, null /* eui48 */);
+ assertEquals(expected, msg.toString());
+ }
+
+ @Test
+ public void testStructClass_hashcode() {
+ final FullTypeMessage msg = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+ ByteOrder.BIG_ENDIAN);
+ final FullTypeMessage msg1 = doParsingMessageTest(FULL_TYPE_DATA, FullTypeMessage.class,
+ ByteOrder.BIG_ENDIAN);
+
+ assertNotEquals(0, msg.hashCode());
+ assertNotEquals(0, msg1.hashCode());
+ assertTrue(msg.equals(msg1));
+ assertEquals(msg.hashCode(), msg1.hashCode());
}
}