IpPrefix improvements.

1. Allow IpPrefixes to be created from strings. In order to do
   this, factor out the code from LinkAddress which already does
   this to a small utility class in NetworkUtils.
2. Truncate prefixes on creation, fixing a TODO.
3. Add a toString method.
4. Write a unit test.

While I'm at it, make RouteInfoTest pass again, and convert it
to use IpPrefix instead of LinkAddress.

Change-Id: I5f68f8af8f4aedb25afaee00e05369f01e82a70b
diff --git a/core/java/android/net/IpPrefix.java b/core/java/android/net/IpPrefix.java
index a14d13f..f1fa3eb 100644
--- a/core/java/android/net/IpPrefix.java
+++ b/core/java/android/net/IpPrefix.java
@@ -18,6 +18,7 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.Pair;
 
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -46,9 +47,18 @@
     private final byte[] address;  // network byte order
     private final int prefixLength;
 
+    private void checkAndMaskAddressAndPrefixLength() {
+        if (address.length != 4 && address.length != 16) {
+            throw new IllegalArgumentException(
+                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
+        }
+        NetworkUtils.maskRawAddress(address, prefixLength);
+    }
+
     /**
      * Constructs a new {@code IpPrefix} from a byte array containing an IPv4 or IPv6 address in
-     * network byte order and a prefix length.
+     * network byte order and a prefix length. Silently truncates the address to the prefix length,
+     * so for example {@code 192.0.2.1/24} is silently converted to {@code 192.0.2.0/24}.
      *
      * @param address the IP address. Must be non-null and exactly 4 or 16 bytes long.
      * @param prefixLength the prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6).
@@ -56,24 +66,46 @@
      * @hide
      */
     public IpPrefix(byte[] address, int prefixLength) {
-        if (address.length != 4 && address.length != 16) {
-            throw new IllegalArgumentException(
-                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
-        }
-        if (prefixLength < 0 || prefixLength > (address.length * 8)) {
-            throw new IllegalArgumentException("IpPrefix with " + address.length +
-                    " bytes has invalid prefix length " + prefixLength);
-        }
         this.address = address.clone();
         this.prefixLength = prefixLength;
-        // TODO: Validate that the non-prefix bits are zero
+        checkAndMaskAddressAndPrefixLength();
     }
 
     /**
+     * Constructs a new {@code IpPrefix} from an IPv4 or IPv6 address and a prefix length. Silently
+     * truncates the address to the prefix length, so for example {@code 192.0.2.1/24} is silently
+     * converted to {@code 192.0.2.0/24}.
+     *
+     * @param address the IP address. Must be non-null.
+     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
      * @hide
      */
     public IpPrefix(InetAddress address, int prefixLength) {
-        this(address.getAddress(), prefixLength);
+        // We don't reuse the (byte[], int) constructor because it calls clone() on the byte array,
+        // which is unnecessary because getAddress() already returns a clone.
+        this.address = address.getAddress();
+        this.prefixLength = prefixLength;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Constructs a new IpPrefix from a string such as "192.0.2.1/24" or "2001:db8::1/64".
+     * Silently truncates the address to the prefix length, so for example {@code 192.0.2.1/24}
+     * is silently converted to {@code 192.0.2.0/24}.
+     *
+     * @param prefix the prefix to parse
+     *
+     * @hide
+     */
+    public IpPrefix(String prefix) {
+        // We don't reuse the (InetAddress, int) constructor because "error: call to this must be
+        // first statement in constructor". We could factor out setting the member variables to an
+        // init() method, but if we did, then we'd have to make the members non-final, or "error:
+        // cannot assign a value to final variable address". So we just duplicate the code here.
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(prefix);
+        this.address = ipAndMask.first.getAddress();
+        this.prefixLength = ipAndMask.second;
+        checkAndMaskAddressAndPrefixLength();
     }
 
     /**
@@ -129,7 +161,7 @@
     }
 
     /**
-     * Returns the prefix length of this {@code IpAddress}.
+     * Returns the prefix length of this {@code IpPrefix}.
      *
      * @return the prefix length.
      */
@@ -138,6 +170,20 @@
     }
 
     /**
+     * Returns a string representation of this {@code IpPrefix}.
+     *
+     * @return a string such as {@code "192.0.2.0/24"} or {@code "2001:db8:1:2::"}.
+     */
+    public String toString() {
+        try {
+            return InetAddress.getByAddress(address).getHostAddress() + "/" + prefixLength;
+        } catch(UnknownHostException e) {
+            // Cosmic rays?
+            throw new IllegalStateException("IpPrefix with invalid address! Shouldn't happen.", e);
+        }
+    }
+
+    /**
      * Implement the Parcelable interface.
      */
     public int describeContents() {
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
index 5246078..f9a25f9 100644
--- a/core/java/android/net/LinkAddress.java
+++ b/core/java/android/net/LinkAddress.java
@@ -18,6 +18,7 @@
 
 import android.os.Parcel;
 import android.os.Parcelable;
+import android.util.Pair;
 
 import java.net.Inet4Address;
 import java.net.InetAddress;
@@ -166,23 +167,9 @@
      * @hide
      */
     public LinkAddress(String address, int flags, int scope) {
-        InetAddress inetAddress = null;
-        int prefixLength = -1;
-        try {
-            String [] pieces = address.split("/", 2);
-            prefixLength = Integer.parseInt(pieces[1]);
-            inetAddress = InetAddress.parseNumericAddress(pieces[0]);
-        } catch (NullPointerException e) {            // Null string.
-        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
-        } catch (NumberFormatException e) {           // Non-numeric prefix.
-        } catch (IllegalArgumentException e) {        // Invalid IP address.
-        }
-
-        if (inetAddress == null || prefixLength == -1) {
-            throw new IllegalArgumentException("Bad LinkAddress params " + address);
-        }
-
-        init(inetAddress, prefixLength, flags, scope);
+        // This may throw an IllegalArgumentException; catching it is the caller's responsibility.
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
+        init(ipAndMask.first, ipAndMask.second, flags, scope);
     }
 
     /**
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index b02f88e..15c0a71 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -24,6 +24,8 @@
 import java.util.Locale;
 
 import android.util.Log;
+import android.util.Pair;
+
 
 /**
  * Native methods for managing network interfaces.
@@ -218,24 +220,17 @@
     }
 
     /**
-     * Get InetAddress masked with prefixLength.  Will never return null.
-     * @param IP address which will be masked with specified prefixLength
-     * @param prefixLength the prefixLength used to mask the IP
+     *  Masks a raw IP address byte array with the specified prefix length.
      */
-    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
-        if (address == null) {
-            throw new RuntimeException("getNetworkPart doesn't accept null address");
-        }
-
-        byte[] array = address.getAddress();
-
+    public static void maskRawAddress(byte[] array, int prefixLength) {
         if (prefixLength < 0 || prefixLength > array.length * 8) {
-            throw new RuntimeException("getNetworkPart - bad prefixLength");
+            throw new RuntimeException("IP address with " + array.length +
+                    " bytes has invalid prefix length " + prefixLength);
         }
 
         int offset = prefixLength / 8;
-        int reminder = prefixLength % 8;
-        byte mask = (byte)(0xFF << (8 - reminder));
+        int remainder = prefixLength % 8;
+        byte mask = (byte)(0xFF << (8 - remainder));
 
         if (offset < array.length) array[offset] = (byte)(array[offset] & mask);
 
@@ -244,6 +239,16 @@
         for (; offset < array.length; offset++) {
             array[offset] = 0;
         }
+    }
+
+    /**
+     * Get InetAddress masked with prefixLength.  Will never return null.
+     * @param address the IP address to mask with
+     * @param prefixLength the prefixLength used to mask the IP
+     */
+    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
+        byte[] array = address.getAddress();
+        maskRawAddress(array, prefixLength);
 
         InetAddress netPart = null;
         try {
@@ -255,6 +260,30 @@
     }
 
     /**
+     * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64".
+     * @hide
+     */
+    public static Pair<InetAddress, Integer> parseIpAndMask(String ipAndMaskString) {
+        InetAddress address = null;
+        int prefixLength = -1;
+        try {
+            String[] pieces = ipAndMaskString.split("/", 2);
+            prefixLength = Integer.parseInt(pieces[1]);
+            address = InetAddress.parseNumericAddress(pieces[0]);
+        } catch (NullPointerException e) {            // Null string.
+        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
+        } catch (NumberFormatException e) {           // Non-numeric prefix.
+        } catch (IllegalArgumentException e) {        // Invalid IP address.
+        }
+
+        if (address == null || prefixLength == -1) {
+            throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString);
+        }
+
+        return new Pair<InetAddress, Integer>(address, prefixLength);
+    }
+
+    /**
      * Check if IP address type is consistent between two InetAddress.
      * @return true if both are the same type.  False otherwise.
      */
diff --git a/core/tests/coretests/src/android/net/IpPrefixTest.java b/core/tests/coretests/src/android/net/IpPrefixTest.java
new file mode 100644
index 0000000..cf278fb
--- /dev/null
+++ b/core/tests/coretests/src/android/net/IpPrefixTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2014 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;
+
+import android.net.IpPrefix;
+import android.os.Parcel;
+import static android.test.MoreAsserts.assertNotEqual;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import static org.junit.Assert.assertArrayEquals;
+import java.net.InetAddress;
+import java.util.Random;
+import junit.framework.TestCase;
+
+
+public class IpPrefixTest extends TestCase {
+
+    // Explicitly cast everything to byte because "error: possible loss of precision".
+    private static final byte[] IPV4_BYTES = { (byte) 192, (byte) 0, (byte) 2, (byte) 4};
+    private static final byte[] IPV6_BYTES = {
+        (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
+        (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
+        (byte) 0x0f, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xa0
+    };
+
+    @SmallTest
+    public void testConstructor() {
+        IpPrefix p;
+        try {
+            p = new IpPrefix((byte[]) null, 9);
+            fail("Expected NullPointerException: null byte array");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix((InetAddress) null, 10);
+            fail("Expected NullPointerException: null InetAddress");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix((String) null);
+            fail("Expected NullPointerException: null String");
+        } catch(RuntimeException expected) {}
+
+
+        try {
+            byte[] b2 = {1, 2, 3, 4, 5};
+            p = new IpPrefix(b2, 29);
+            fail("Expected IllegalArgumentException: invalid array length");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1.2.3.4");
+            fail("Expected IllegalArgumentException: no prefix length");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1.2.3.4/");
+            fail("Expected IllegalArgumentException: empty prefix length");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("foo/32");
+            fail("Expected IllegalArgumentException: invalid address");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1/32");
+            fail("Expected IllegalArgumentException: deprecated IPv4 format");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("1.2.3.256/32");
+            fail("Expected IllegalArgumentException: invalid IPv4 address");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("foo/32");
+            fail("Expected IllegalArgumentException: non-address");
+        } catch(IllegalArgumentException expected) {}
+
+        try {
+            p = new IpPrefix("f00:::/32");
+            fail("Expected IllegalArgumentException: invalid IPv6 address");
+        } catch(IllegalArgumentException expected) {}
+    }
+
+    public void testTruncation() {
+        IpPrefix p;
+
+        p = new IpPrefix(IPV4_BYTES, 32);
+        assertEquals("192.0.2.4/32", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 29);
+        assertEquals("192.0.2.0/29", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 8);
+        assertEquals("192.0.0.0/8", p.toString());
+
+        p = new IpPrefix(IPV4_BYTES, 0);
+        assertEquals("0.0.0.0/0", p.toString());
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, 33);
+            fail("Expected IllegalArgumentException: invalid prefix length");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, 128);
+            fail("Expected IllegalArgumentException: invalid prefix length");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix(IPV4_BYTES, -1);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch(RuntimeException expected) {}
+
+        p = new IpPrefix(IPV6_BYTES, 128);
+        assertEquals("2001:db8:dead:beef:f00::a0/128", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 122);
+        assertEquals("2001:db8:dead:beef:f00::80/122", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 64);
+        assertEquals("2001:db8:dead:beef::/64", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 3);
+        assertEquals("2000::/3", p.toString());
+
+        p = new IpPrefix(IPV6_BYTES, 0);
+        assertEquals("::/0", p.toString());
+
+        try {
+            p = new IpPrefix(IPV6_BYTES, -1);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch(RuntimeException expected) {}
+
+        try {
+            p = new IpPrefix(IPV6_BYTES, 129);
+            fail("Expected IllegalArgumentException: negative prefix length");
+        } catch(RuntimeException expected) {}
+
+    }
+
+    private void assertAreEqual(Object o1, Object o2) {
+        assertTrue(o1.equals(o2));
+        assertTrue(o2.equals(o1));
+    }
+
+    private void assertAreNotEqual(Object o1, Object o2) {
+        assertFalse(o1.equals(o2));
+        assertFalse(o2.equals(o1));
+    }
+
+    @SmallTest
+    public void testEquals() {
+        IpPrefix p1, p2;
+
+        p1 = new IpPrefix("192.0.2.251/23");
+        p2 = new IpPrefix(new byte[]{(byte) 192, (byte) 0, (byte) 2, (byte) 251}, 23);
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("192.0.2.5/23");
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("192.0.2.5/24");
+        assertAreNotEqual(p1, p2);
+
+        p1 = new IpPrefix("192.0.4.5/23");
+        assertAreNotEqual(p1, p2);
+
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::80/122");
+        p2 = new IpPrefix(IPV6_BYTES, 122);
+        assertEquals("2001:db8:dead:beef:f00::80/122", p2.toString());
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::bf/122");
+        assertAreEqual(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef:f00::8:0/123");
+        assertAreNotEqual(p1, p2);
+
+        p1 = new IpPrefix("2001:db8:dead:beef::/122");
+        assertAreNotEqual(p1, p2);
+
+        // 192.0.2.4/32 != c000:0204::/32.
+        byte[] ipv6bytes = new byte[16];
+        System.arraycopy(IPV4_BYTES, 0, ipv6bytes, 0, IPV4_BYTES.length);
+        p1 = new IpPrefix(ipv6bytes, 32);
+        assertAreEqual(p1, new IpPrefix("c000:0204::/32"));
+
+        p2 = new IpPrefix(IPV4_BYTES, 32);
+        assertAreNotEqual(p1, p2);
+    }
+
+    @SmallTest
+    public void testHashCode() {
+        IpPrefix p;
+        int oldCode = -1;
+        Random random = new Random();
+        for (int i = 0; i < 100; i++) {
+            if (random.nextBoolean()) {
+                // IPv4.
+                byte[] b = new byte[4];
+                random.nextBytes(b);
+                p = new IpPrefix(b, random.nextInt(33));
+                assertNotEqual(oldCode, p.hashCode());
+                oldCode = p.hashCode();
+            } else {
+                // IPv6.
+                byte[] b = new byte[16];
+                random.nextBytes(b);
+                p = new IpPrefix(b, random.nextInt(129));
+                assertNotEqual(oldCode, p.hashCode());
+                oldCode = p.hashCode();
+            }
+        }
+    }
+
+    @SmallTest
+    public void testMappedAddressesAreBroken() {
+        // 192.0.2.0/24 != ::ffff:c000:0204/120, but because we use InetAddress,
+        // we are unable to comprehend that.
+        byte[] ipv6bytes = {
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
+            (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff,
+            (byte) 192, (byte) 0, (byte) 2, (byte) 0};
+        IpPrefix p = new IpPrefix(ipv6bytes, 120);
+        assertEquals(16, p.getRawAddress().length);       // Fine.
+        assertArrayEquals(ipv6bytes, p.getRawAddress());  // Fine.
+
+        // Broken.
+        assertEquals("192.0.2.0/120", p.toString());
+        assertEquals(InetAddress.parseNumericAddress("192.0.2.0"), p.getAddress());
+    }
+
+    public IpPrefix passThroughParcel(IpPrefix p) {
+        Parcel parcel = Parcel.obtain();
+        IpPrefix p2 = null;
+        try {
+            p.writeToParcel(parcel, 0);
+            parcel.setDataPosition(0);
+            p2 = IpPrefix.CREATOR.createFromParcel(parcel);
+        } finally {
+            parcel.recycle();
+        }
+        assertNotNull(p2);
+        return p2;
+    }
+
+    public void assertParcelingIsLossless(IpPrefix p) {
+      IpPrefix p2 = passThroughParcel(p);
+      assertEquals(p, p2);
+    }
+
+    public void testParceling() {
+        IpPrefix p;
+
+        p = new IpPrefix("2001:4860:db8::/64");
+        assertParcelingIsLossless(p);
+
+        p = new IpPrefix("192.0.2.0/25");
+        assertParcelingIsLossless(p);
+    }
+}
diff --git a/core/tests/coretests/src/android/net/RouteInfoTest.java b/core/tests/coretests/src/android/net/RouteInfoTest.java
index c80d0bf..af6a32b 100644
--- a/core/tests/coretests/src/android/net/RouteInfoTest.java
+++ b/core/tests/coretests/src/android/net/RouteInfoTest.java
@@ -19,7 +19,7 @@
 import java.lang.reflect.Method;
 import java.net.InetAddress;
 
-import android.net.LinkAddress;
+import android.net.IpPrefix;
 import android.net.RouteInfo;
 import android.os.Parcel;
 
@@ -32,9 +32,8 @@
         return InetAddress.parseNumericAddress(addr);
     }
 
-    private LinkAddress Prefix(String prefix) {
-        String[] parts = prefix.split("/");
-        return new LinkAddress(Address(parts[0]), Integer.parseInt(parts[1]));
+    private IpPrefix Prefix(String prefix) {
+        return new IpPrefix(prefix);
     }
 
     @SmallTest
@@ -43,17 +42,17 @@
 
         // Invalid input.
         try {
-            r = new RouteInfo((LinkAddress) null, null, "rmnet0");
+            r = new RouteInfo((IpPrefix) null, null, "rmnet0");
             fail("Expected RuntimeException:  destination and gateway null");
         } catch(RuntimeException e) {}
 
         // Null destination is default route.
-        r = new RouteInfo((LinkAddress) null, Address("2001:db8::1"), null);
+        r = new RouteInfo((IpPrefix) null, Address("2001:db8::1"), null);
         assertEquals(Prefix("::/0"), r.getDestination());
         assertEquals(Address("2001:db8::1"), r.getGateway());
         assertNull(r.getInterface());
 
-        r = new RouteInfo((LinkAddress) null, Address("192.0.2.1"), "wlan0");
+        r = new RouteInfo((IpPrefix) null, Address("192.0.2.1"), "wlan0");
         assertEquals(Prefix("0.0.0.0/0"), r.getDestination());
         assertEquals(Address("192.0.2.1"), r.getGateway());
         assertEquals("wlan0", r.getInterface());
@@ -74,7 +73,7 @@
         class PatchedRouteInfo {
             private final RouteInfo mRouteInfo;
 
-            public PatchedRouteInfo(LinkAddress destination, InetAddress gateway, String iface) {
+            public PatchedRouteInfo(IpPrefix destination, InetAddress gateway, String iface) {
                 mRouteInfo = new RouteInfo(destination, gateway, iface);
             }