Parse and validate txt records.

Bug: 27696905
Change-Id: I9affcf02a51c92a2be1c2bfc5efbd09065e100bc
diff --git a/core/java/android/net/nsd/NsdServiceInfo.java b/core/java/android/net/nsd/NsdServiceInfo.java
index 6fdb0d0..4a06fb1 100644
--- a/core/java/android/net/nsd/NsdServiceInfo.java
+++ b/core/java/android/net/nsd/NsdServiceInfo.java
@@ -16,8 +16,11 @@
 
 package android.net.nsd;
 
+import android.annotation.NonNull;
 import android.os.Parcelable;
 import android.os.Parcel;
+import android.text.TextUtils;
+import android.util.Base64;
 import android.util.Log;
 import android.util.ArrayMap;
 
@@ -95,8 +98,99 @@
         mPort = p;
     }
 
+    /**
+     * Unpack txt information from a base-64 encoded byte array.
+     *
+     * @param rawRecords The raw base64 encoded records string read from netd.
+     *
+     * @hide
+     */
+    public void setTxtRecords(@NonNull String rawRecords) {
+        byte[] txtRecordsRawBytes = Base64.decode(rawRecords, Base64.DEFAULT);
+
+        // There can be multiple TXT records after each other. Each record has to following format:
+        //
+        // byte                  type                  required   meaning
+        // -------------------   -------------------   --------   ----------------------------------
+        // 0                     unsigned 8 bit        yes        size of record excluding this byte
+        // 1 - n                 ASCII but not '='     yes        key
+        // n + 1                 '='                   optional   separator of key and value
+        // n + 2 - record size   uninterpreted bytes   optional   value
+        //
+        // Example legal records:
+        // [11, 'm', 'y', 'k', 'e', 'y', '=', 0x0, 0x4, 0x65, 0x7, 0xff]
+        // [17, 'm', 'y', 'K', 'e', 'y', 'W', 'i', 't', 'h', 'N', 'o', 'V', 'a', 'l', 'u', 'e', '=']
+        // [12, 'm', 'y', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'K', 'e', 'y']
+        //
+        // Example corrupted records
+        // [3, =, 1, 2]    <- key is empty
+        // [3, 0, =, 2]    <- key contains non-ASCII character. We handle this by replacing the
+        //                    invalid characters instead of skipping the record.
+        // [30, 'a', =, 2] <- length exceeds total left over bytes in the TXT records array, we
+        //                    handle this by reducing the length of the record as needed.
+        int pos = 0;
+        while (pos < txtRecordsRawBytes.length) {
+            // recordLen is an unsigned 8 bit value
+            int recordLen = txtRecordsRawBytes[pos] & 0xff;
+            pos += 1;
+
+            try {
+                if (recordLen == 0) {
+                    throw new IllegalArgumentException("Zero sized txt record");
+                } else if (pos + recordLen > txtRecordsRawBytes.length) {
+                    Log.w(TAG, "Corrupt record length (pos = " + pos + "): " + recordLen);
+                    recordLen = txtRecordsRawBytes.length - pos;
+                }
+
+                // Decode key-value records
+                String key = null;
+                byte[] value = null;
+                int valueLen = 0;
+                for (int i = pos; i < pos + recordLen; i++) {
+                    if (key == null) {
+                        if (txtRecordsRawBytes[i] == '=') {
+                            key = new String(txtRecordsRawBytes, pos, i - pos,
+                                    StandardCharsets.US_ASCII);
+                        }
+                    } else {
+                        if (value == null) {
+                            value = new byte[recordLen - key.length() - 1];
+                        }
+                        value[valueLen] = txtRecordsRawBytes[i];
+                        valueLen++;
+                    }
+                }
+
+                // If '=' was not found we have a boolean record
+                if (key == null) {
+                    key = new String(txtRecordsRawBytes, pos, recordLen, StandardCharsets.US_ASCII);
+                }
+
+                if (TextUtils.isEmpty(key)) {
+                    // Empty keys are not allowed (RFC6763 6.4)
+                    throw new IllegalArgumentException("Invalid txt record (key is empty)");
+                }
+
+                if (getAttributes().containsKey(key)) {
+                    // When we have a duplicate record, the later ones are ignored (RFC6763 6.4)
+                    throw new IllegalArgumentException("Invalid txt record (duplicate key \"" + key + "\")");
+                }
+
+                setAttribute(key, value);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "While parsing txt records (pos = " + pos + "): " + e.getMessage());
+            }
+
+            pos += recordLen;
+        }
+    }
+
     /** @hide */
     public void setAttribute(String key, byte[] value) {
+        if (TextUtils.isEmpty(key)) {
+            throw new IllegalArgumentException("Key cannot be empty");
+        }
+
         // Key must be printable US-ASCII, excluding =.
         for (int i = 0; i < key.length(); ++i) {
             char character = key.charAt(i);
@@ -177,10 +271,10 @@
     }
 
     /** @hide */
-    public byte[] getTxtRecord() {
+    public @NonNull byte[] getTxtRecord() {
         int txtRecordSize = getTxtRecordSize();
         if (txtRecordSize == 0) {
-            return null;
+            return new byte[]{};
         }
 
         byte[] txtRecord = new byte[txtRecordSize];
diff --git a/services/core/java/com/android/server/NsdService.java b/services/core/java/com/android/server/NsdService.java
index 11aef17..a44b065 100644
--- a/services/core/java/com/android/server/NsdService.java
+++ b/services/core/java/com/android/server/NsdService.java
@@ -30,16 +30,14 @@
 import android.os.Messenger;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.Base64;
 import android.util.Slog;
 import android.util.SparseArray;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.net.InetAddress;
 import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 
 import com.android.internal.util.AsyncChannel;
@@ -492,6 +490,7 @@
                         clientInfo.mResolvedService.setServiceName(name);
                         clientInfo.mResolvedService.setServiceType(type);
                         clientInfo.mResolvedService.setPort(Integer.parseInt(cooked[4]));
+                        clientInfo.mResolvedService.setTxtRecords(cooked[6]);
 
                         stopResolveService(id);
                         removeRequestMap(clientId, id, clientInfo);
@@ -708,20 +707,9 @@
         if (DBG) Slog.d(TAG, "registerService: " + regId + " " + service);
         try {
             Command cmd = new Command("mdnssd", "register", regId, service.getServiceName(),
-                    service.getServiceType(), service.getPort());
-
-            // Add TXT records as additional arguments.
-            Map<String, byte[]> txtRecords = service.getAttributes();
-            for (String key : txtRecords.keySet()) {
-                try {
-                    // TODO: Send encoded TXT record as bytes once NDC/netd supports binary data.
-                    byte[] recordValue = txtRecords.get(key);
-                    cmd.appendArg(String.format(Locale.US, "%s=%s", key,
-                            recordValue != null ? new String(recordValue, "UTF_8") : ""));
-                } catch (UnsupportedEncodingException e) {
-                    Slog.e(TAG, "Failed to encode txtRecord " + e);
-                }
-            }
+                    service.getServiceType(), service.getPort(),
+                    Base64.encodeToString(service.getTxtRecord(), Base64.DEFAULT)
+                            .replace("\n", ""));
 
             mNativeConnector.execute(cmd);
         } catch(NativeDaemonConnectorException e) {