Add fromBase64EncodedString to BpfDump

EthernetTetheringTest has parseMapKeyValue that decodes the string
encoded by BpfDump#toBase64EncodedString
Upcoming CL also wants to decode the encoded string.
So this CL renames parseMapKeyValue to fromBase64EncodedString and moves
to BpfDump with refactoring

Bug: 217624062
Test: atest NetworkStaticLibTests
Change-Id: I6ec302aaaa3e8779119cf153567cb032828f5daf
diff --git a/staticlibs/device/com/android/net/module/util/BpfDump.java b/staticlibs/device/com/android/net/module/util/BpfDump.java
index fec225c..67e9c93 100644
--- a/staticlibs/device/com/android/net/module/util/BpfDump.java
+++ b/staticlibs/device/com/android/net/module/util/BpfDump.java
@@ -16,16 +16,20 @@
 package com.android.net.module.util;
 
 import android.util.Base64;
+import android.util.Pair;
 
 import androidx.annotation.NonNull;
 
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
 /**
  * The classes and the methods for BPF dump utilization.
  */
 public class BpfDump {
     // Using "," as a separator between base64 encoded key and value is safe because base64
     // characters are [0-9a-zA-Z/=+].
-    public static final String BASE64_DELIMITER = ",";
+    private static final String BASE64_DELIMITER = ",";
 
     /**
      * Encode BPF key and value into a base64 format string which uses the delimiter ',':
@@ -43,6 +47,33 @@
         return keyBase64Str + BASE64_DELIMITER + valueBase64Str;
     }
 
+    /**
+     * Decode Struct from a base64 format string
+     */
+    private static <T extends Struct> T parseStruct(
+            Class<T> structClass, @NonNull String base64Str) {
+        final byte[] bytes = Base64.decode(base64Str, Base64.DEFAULT);
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        return Struct.parse(structClass, byteBuffer);
+    }
+
+    /**
+     * Decode BPF key and value from a base64 format string which uses the delimiter ',':
+     * <base64 encoded key>,<base64 encoded value>
+     */
+    public static <K extends Struct, V extends Struct> Pair<K, V> fromBase64EncodedString(
+            Class<K> keyClass, Class<V> valueClass, @NonNull String base64Str) {
+        String[] keyValueStrs = base64Str.split(BASE64_DELIMITER);
+        if (keyValueStrs.length != 2 /* key + value */) {
+            throw new IllegalArgumentException("Invalid base64Str (" + base64Str + "), base64Str"
+                    + " must contain exactly one delimiter '" + BASE64_DELIMITER + "'");
+        }
+        final K k = parseStruct(keyClass, keyValueStrs[0]);
+        final V v = parseStruct(valueClass, keyValueStrs[1]);
+        return new Pair<>(k, v);
+    }
+
     // TODO: add a helper to dump bpf map content with the map name, the header line
     // (ex: "BPF ingress map: iif nat64Prefix v6Addr -> v4Addr oif"), a lambda that
     // knows how to dump each line, and the PrintWriter.
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
index b166b4a..395011c 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/BpfDumpTest.java
@@ -17,6 +17,9 @@
 package com.android.net.module.util;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import android.util.Pair;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -27,17 +30,53 @@
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class BpfDumpTest {
+    private static final int TEST_KEY = 123;
+    private static final String TEST_KEY_BASE64 = "ewAAAA==";
+    private static final int TEST_VAL = 456;
+    private static final String TEST_VAL_BASE64 = "yAEAAA==";
+    private static final String BASE64_DELIMITER = ",";
+    private static final String TEST_KEY_VAL_BASE64 =
+            TEST_KEY_BASE64 + BASE64_DELIMITER + TEST_VAL_BASE64;
+    private static final String INVALID_BASE64_STRING = "Map is null";
+
     @Test
     public void testToBase64EncodedString() {
-        final Struct.U32 key = new Struct.U32(123);
-        final Struct.U32 value = new Struct.U32(456);
+        final Struct.U32 key = new Struct.U32(TEST_KEY);
+        final Struct.U32 value = new Struct.U32(TEST_VAL);
 
         // Verified in python:
         //   import base64
-        //   print(base64.b64encode(b'\x7b\x00\x00\x00')) # key: ewAAAA==
-        //   print(base64.b64encode(b'\xc8\x01\x00\x00')) # value: yAEAAA==
+        //   print(base64.b64encode(b'\x7b\x00\x00\x00')) # key: ewAAAA== (TEST_KEY_BASE64)
+        //   print(base64.b64encode(b'\xc8\x01\x00\x00')) # value: yAEAAA== (TEST_VAL_BASE64)
         assertEquals("7B000000", HexDump.toHexString(key.writeToBytes()));
         assertEquals("C8010000", HexDump.toHexString(value.writeToBytes()));
-        assertEquals("ewAAAA==,yAEAAA==", BpfDump.toBase64EncodedString(key, value));
+        assertEquals(TEST_KEY_VAL_BASE64, BpfDump.toBase64EncodedString(key, value));
+    }
+
+    @Test
+    public void testFromBase64EncodedString() {
+        Pair<Struct.U32, Struct.U32> decodedKeyValue = BpfDump.fromBase64EncodedString(
+                Struct.U32.class, Struct.U32.class, TEST_KEY_VAL_BASE64);
+        assertEquals(TEST_KEY, decodedKeyValue.first.val);
+        assertEquals(TEST_VAL, decodedKeyValue.second.val);
+    }
+
+    private void assertThrowsIllegalArgumentException(final String testStr) {
+        assertThrows(IllegalArgumentException.class,
+                () -> BpfDump.fromBase64EncodedString(Struct.U32.class, Struct.U32.class, testStr));
+    }
+
+    @Test
+    public void testFromBase64EncodedStringInvalidString() {
+        assertThrowsIllegalArgumentException(INVALID_BASE64_STRING);
+        assertThrowsIllegalArgumentException(TEST_KEY_BASE64);
+        assertThrowsIllegalArgumentException(
+                TEST_KEY_BASE64 + BASE64_DELIMITER + INVALID_BASE64_STRING);
+        assertThrowsIllegalArgumentException(
+                INVALID_BASE64_STRING + BASE64_DELIMITER + TEST_VAL_BASE64);
+        assertThrowsIllegalArgumentException(
+                INVALID_BASE64_STRING + BASE64_DELIMITER + INVALID_BASE64_STRING);
+        assertThrowsIllegalArgumentException(
+                TEST_KEY_VAL_BASE64 + BASE64_DELIMITER + TEST_KEY_BASE64);
     }
 }