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());
     }
 }