Add test utils for SVCB packet generation.

This is based on patchset 22 of aosp/2453209.

Bug: 240259333
Test: test-only code, will be covered by NetworkMonitorTest
Change-Id: Ibede3fb519e6f1d0c3d99886f553ee006ddfebc9
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
new file mode 100644
index 0000000..8608344
--- /dev/null
+++ b/staticlibs/testutils/devicetests/com/android/testutils/DnsSvcbUtils.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 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 com.android.testutils;
+
+import static android.net.DnsResolver.CLASS_IN;
+
+import static com.android.net.module.util.DnsPacket.TYPE_SVCB;
+import static com.android.net.module.util.DnsPacketUtils.DnsRecordParser.domainNameToLabels;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+
+import static org.junit.Assert.fail;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import android.net.InetAddresses;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class DnsSvcbUtils {
+    private static final Pattern SVC_PARAM_PATTERN = Pattern.compile("([a-z0-9-]+)=?(.*)");
+
+    /**
+     * Returns a DNS SVCB response with given hostname `hostname` and given SVCB records
+     * `records`. Each record must contain the service priority, the target name, and the service
+     * parameters.
+     *     E.g. "1 doh.google alpn=h2,h3 port=443 ipv4hint=192.0.2.1 dohpath=/dns-query{?dns}"
+     */
+    @NonNull
+    public static byte[] makeSvcbResponse(String hostname, String[] records) throws IOException {
+        if (records == null) throw new NullPointerException();
+        if (!hostname.startsWith("_dns.")) throw new UnsupportedOperationException();
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        // Write DNS header.
+        os.write(shortsToByteArray(
+                0x1234,         /* Transaction ID */
+                0x8100,         /* Flags */
+                1,              /* qdcount */
+                records.length, /* ancount */
+                0,              /* nscount */
+                0               /* arcount */
+        ));
+        // Write Question.
+        // - domainNameToLabels() doesn't support the hostname starting with "_", so divide
+        //   the writing into two steps.
+        os.write(new byte[] { 0x04, '_', 'd', 'n', 's' });
+        os.write(domainNameToLabels(hostname.substring(5)));
+        os.write(shortsToByteArray(TYPE_SVCB, CLASS_IN));
+        // Write Answer section.
+        for (String r : records) {
+            os.write(makeSvcbRecord(r));
+        }
+        return os.toByteArray();
+    }
+
+    @NonNull
+    private static byte[] makeSvcbRecord(String representation) throws IOException {
+        if (representation == null) return new byte[0];
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(shortsToByteArray(
+                0xc00c, /* Pointer to qname in question section */
+                TYPE_SVCB,
+                CLASS_IN,
+                0, 16, /* TTL = 16 */
+                0 /* Data Length = 0 */
+
+        ));
+        final String[] strings = representation.split(" +");
+        // SvcPriority and TargetName are mandatory in the representation.
+        if (strings.length < 3) {
+            fail("Invalid SVCB representation: " + representation);
+        }
+        // Write SvcPriority, TargetName, and SvcParams.
+        os.write(shortsToByteArray(Short.parseShort(strings[0])));
+        os.write(domainNameToLabels(strings[1]));
+        for (int i = 2; i < strings.length; i++) {
+            try {
+                os.write(svcParamToByteArray(strings[i]));
+            } catch (UnsupportedEncodingException e) {
+                throw new IOException(e);
+            }
+        }
+        // Update rdata length.
+        final byte[] out = os.toByteArray();
+        ByteBuffer.wrap(out).putShort(10, (short) (out.length - 12));
+        return out;
+    }
+
+    @NonNull
+    private static byte[] svcParamToByteArray(String svcParam) throws IOException {
+        final Matcher matcher = SVC_PARAM_PATTERN.matcher(svcParam);
+        if (!matcher.matches() || matcher.groupCount() != 2) {
+            fail("Invalid SvcParam: " + svcParam);
+        }
+        final String svcParamkey = matcher.group(1);
+        final String svcParamValue = matcher.group(2);
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        os.write(svcParamKeyToBytes(svcParamkey));
+        switch (svcParamkey) {
+            case "mandatory":
+                final String[] keys = svcParamValue.split(",");
+                os.write(shortsToByteArray(keys.length));
+                for (String v : keys) {
+                    os.write(svcParamKeyToBytes(v));
+                }
+                break;
+            case "alpn":
+                os.write(shortsToByteArray((svcParamValue.length() + 1)));
+                for (String v : svcParamValue.split(",")) {
+                    os.write(v.length());
+                    // TODO: support percent-encoding per RFC 7838.
+                    os.write(v.getBytes(US_ASCII));
+                }
+                break;
+            case "no-default-alpn":
+                os.write(shortsToByteArray(0));
+                break;
+            case "port":
+                os.write(shortsToByteArray(2));
+                os.write(shortsToByteArray(Short.parseShort(svcParamValue)));
+                break;
+            case "ipv4hint":
+                final String[] v4Addrs = svcParamValue.split(",");
+                os.write(shortsToByteArray((v4Addrs.length * IPV4_ADDR_LEN)));
+                for (String v : v4Addrs) {
+                    os.write(InetAddresses.parseNumericAddress(v).getAddress());
+                }
+                break;
+            case "ech":
+                os.write(shortsToByteArray(svcParamValue.length()));
+                os.write(svcParamValue.getBytes(US_ASCII));  // base64 encoded
+                break;
+            case "ipv6hint":
+                final String[] v6Addrs = svcParamValue.split(",");
+                os.write(shortsToByteArray((v6Addrs.length * IPV6_ADDR_LEN)));
+                for (String v : v6Addrs) {
+                    os.write(InetAddresses.parseNumericAddress(v).getAddress());
+                }
+                break;
+            case "dohpath":
+                os.write(shortsToByteArray(svcParamValue.length()));
+                // TODO: support percent-encoding, since this is a URI template.
+                os.write(svcParamValue.getBytes(US_ASCII));
+                break;
+            default:
+                os.write(shortsToByteArray(svcParamValue.length()));
+                os.write(svcParamValue.getBytes(US_ASCII));
+                break;
+        }
+        return os.toByteArray();
+    }
+
+    @NonNull
+    private static byte[] svcParamKeyToBytes(String key) {
+        switch (key) {
+            case "mandatory": return shortsToByteArray(0);
+            case "alpn": return shortsToByteArray(1);
+            case "no-default-alpn": return shortsToByteArray(2);
+            case "port": return shortsToByteArray(3);
+            case "ipv4hint": return shortsToByteArray(4);
+            case "ech": return shortsToByteArray(5);
+            case "ipv6hint": return shortsToByteArray(6);
+            case "dohpath": return shortsToByteArray(7);
+            default:
+                if (!key.startsWith("key")) fail("Invalid SvcParamKey " + key);
+                return shortsToByteArray(Short.parseShort(key.substring(3)));
+        }
+    }
+
+    @NonNull
+    private static byte[] shortsToByteArray(int... values) {
+        final ByteBuffer out = ByteBuffer.allocate(values.length * 2);
+        for (int value: values) {
+            if (value < 0 || value > 0xffff) {
+                throw new AssertionError("not an unsigned short: " + value);
+            }
+            out.putShort((short) value);
+        }
+        return out.array();
+    }
+}