[Feature sync] fix handling arbitrary bytes in TXT value
RFC 6763 defines that TXT value can accept both utf-8 string and
binary data. Current implementation will always cast the TXT
value to a utf-8 string and will cause data lose when there are
non-utf-8 chars in the TXT value. This commit fixes this by
having the browser passing the TXT values back as byte[].
Also fixed the TXT key&value parsing issues per RFC 6763
section 6.5: accept cases of no '=' and reject empty key.
Bug: 254155029
Test: atest FrameworksNetTests CtsNetTestCases
Change-Id: I4b755e60ad6e59db19faa41556dd214993d73896
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
index 61c5f5a..856a2cd 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java
@@ -16,8 +16,11 @@
package com.android.server.connectivity.mdns;
+import android.annotation.Nullable;
import android.util.SparseArray;
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+
import java.io.EOFException;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -195,6 +198,16 @@
return val;
}
+ @Nullable
+ public TextEntry readTextEntry() throws EOFException {
+ int len = readUInt8();
+ checkRemaining(len);
+ byte[] bytes = new byte[len];
+ System.arraycopy(buf, pos, bytes, 0, bytes.length);
+ pos += len;
+ return TextEntry.fromBytes(bytes);
+ }
+
/**
* Reads a specific number of bytes.
*
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
index 2fed36d..b78aa5d 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketWriter.java
@@ -16,6 +16,8 @@
package com.android.server.connectivity.mdns;
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.SocketAddress;
@@ -147,6 +149,12 @@
writeBytes(utf8);
}
+ public void writeTextEntry(TextEntry textEntry) throws IOException {
+ byte[] bytes = textEntry.toBytes();
+ writeUInt8(bytes.length);
+ writeBytes(bytes);
+ }
+
/**
* Writes a series of labels. Uses name compression.
*
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
index 2e4a4e5..d142280 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceInfo.java
@@ -17,11 +17,16 @@
package com.android.server.connectivity.mdns;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import com.android.net.module.util.ByteUtils;
+
+import java.nio.charset.Charset;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -34,6 +39,8 @@
* @hide
*/
public class MdnsServiceInfo implements Parcelable {
+ private static final Charset US_ASCII = Charset.forName("us-ascii");
+ private static final Charset UTF_8 = Charset.forName("utf-8");
/** @hide */
public static final Parcelable.Creator<MdnsServiceInfo> CREATOR =
@@ -49,7 +56,8 @@
source.readInt(),
source.readString(),
source.readString(),
- source.createStringArrayList());
+ source.createStringArrayList(),
+ source.createTypedArrayList(TextEntry.CREATOR));
}
@Override
@@ -65,8 +73,33 @@
private final int port;
private final String ipv4Address;
private final String ipv6Address;
- private final Map<String, String> attributes = new HashMap<>();
- List<String> textStrings;
+ final List<String> textStrings;
+ @Nullable
+ final List<TextEntry> textEntries;
+
+ private final Map<String, byte[]> attributes;
+
+ /** Constructs a {@link MdnsServiceInfo} object with default values. */
+ public MdnsServiceInfo(
+ String serviceInstanceName,
+ String[] serviceType,
+ List<String> subtypes,
+ String[] hostName,
+ int port,
+ String ipv4Address,
+ String ipv6Address,
+ List<String> textStrings) {
+ this(
+ serviceInstanceName,
+ serviceType,
+ subtypes,
+ hostName,
+ port,
+ ipv4Address,
+ ipv6Address,
+ textStrings,
+ /* textEntries= */ null);
+ }
/**
* Constructs a {@link MdnsServiceInfo} object with default values.
@@ -81,7 +114,8 @@
int port,
String ipv4Address,
String ipv6Address,
- List<String> textStrings) {
+ List<String> textStrings,
+ @Nullable List<TextEntry> textEntries) {
this.serviceInstanceName = serviceInstanceName;
this.serviceType = serviceType;
this.subtypes = new ArrayList<>();
@@ -92,16 +126,40 @@
this.port = port;
this.ipv4Address = ipv4Address;
this.ipv6Address = ipv6Address;
+ this.textStrings = new ArrayList<>();
if (textStrings != null) {
- for (String text : textStrings) {
- int pos = text.indexOf('=');
- if (pos < 1) {
- continue;
- }
- attributes.put(text.substring(0, pos).toLowerCase(Locale.ENGLISH),
- text.substring(++pos));
+ this.textStrings.addAll(textStrings);
+ }
+ this.textEntries = (textEntries == null) ? null : new ArrayList<>(textEntries);
+
+ // The module side sends both {@code textStrings} and {@code textEntries} for backward
+ // compatibility. We should prefer only {@code textEntries} if it's not null.
+ List<TextEntry> entries =
+ (this.textEntries != null) ? this.textEntries : parseTextStrings(this.textStrings);
+ Map<String, byte[]> attributes = new HashMap<>(entries.size());
+ for (TextEntry entry : entries) {
+ String key = entry.getKey().toLowerCase(Locale.ENGLISH);
+
+ // Per https://datatracker.ietf.org/doc/html/rfc6763#section-6.4, only the first entry
+ // of the same key should be accepted:
+ // If a client receives a TXT record containing the same key more than once, then the
+ // client MUST silently ignore all but the first occurrence of that attribute.
+ if (!attributes.containsKey(key)) {
+ attributes.put(key, entry.getValue());
}
}
+ this.attributes = Collections.unmodifiableMap(attributes);
+ }
+
+ private static List<TextEntry> parseTextStrings(List<String> textStrings) {
+ List<TextEntry> list = new ArrayList(textStrings.size());
+ for (String textString : textStrings) {
+ TextEntry entry = TextEntry.fromString(textString);
+ if (entry != null) {
+ list.add(entry);
+ }
+ }
+ return Collections.unmodifiableList(list);
}
/** @return the name of this service instance. */
@@ -148,16 +206,35 @@
}
/**
- * @return the attribute value for {@code key}.
- * @return {@code null} if no attribute value exists for {@code key}.
+ * Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure
+ * that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no
+ * attribute value exists for {@code key}.
*/
+ @Nullable
public String getAttributeByKey(@NonNull String key) {
+ byte[] value = getAttributeAsBytes(key);
+ if (value == null) {
+ return null;
+ }
+ return new String(value, UTF_8);
+ }
+
+ /**
+ * Returns the attribute value for {@code key} as a byte array. {@code null} will be returned if
+ * no attribute value exists for {@code key}.
+ */
+ @Nullable
+ public byte[] getAttributeAsBytes(@NonNull String key) {
return attributes.get(key.toLowerCase(Locale.ENGLISH));
}
/** @return an immutable map of all attributes. */
public Map<String, String> getAttributes() {
- return Collections.unmodifiableMap(attributes);
+ Map<String, String> map = new HashMap<>(attributes.size());
+ for (Map.Entry<String, byte[]> kv : attributes.entrySet()) {
+ map.put(kv.getKey(), new String(kv.getValue(), UTF_8));
+ }
+ return Collections.unmodifiableMap(map);
}
@Override
@@ -167,14 +244,6 @@
@Override
public void writeToParcel(Parcel out, int flags) {
- if (textStrings == null) {
- // Lazily initialize the parcelable field mTextStrings.
- textStrings = new ArrayList<>(attributes.size());
- for (Map.Entry<String, String> kv : attributes.entrySet()) {
- textStrings.add(String.format(Locale.ROOT, "%s=%s", kv.getKey(), kv.getValue()));
- }
- }
-
out.writeString(serviceInstanceName);
out.writeStringArray(serviceType);
out.writeStringList(subtypes);
@@ -183,6 +252,7 @@
out.writeString(ipv4Address);
out.writeString(ipv6Address);
out.writeStringList(textStrings);
+ out.writeTypedList(textEntries);
}
@Override
@@ -195,4 +265,114 @@
ipv4Address,
port);
}
+
+
+ /** Represents a DNS TXT key-value pair defined by RFC 6763. */
+ public static final class TextEntry implements Parcelable {
+ public static final Parcelable.Creator<TextEntry> CREATOR =
+ new Parcelable.Creator<TextEntry>() {
+ @Override
+ public TextEntry createFromParcel(Parcel source) {
+ return new TextEntry(source);
+ }
+
+ @Override
+ public TextEntry[] newArray(int size) {
+ return new TextEntry[size];
+ }
+ };
+
+ private final String key;
+ private final byte[] value;
+
+ /** Creates a new {@link TextEntry} instance from a '=' separated string. */
+ @Nullable
+ public static TextEntry fromString(String textString) {
+ return fromBytes(textString.getBytes(UTF_8));
+ }
+
+ /** Creates a new {@link TextEntry} instance from a '=' separated byte array. */
+ @Nullable
+ public static TextEntry fromBytes(byte[] textBytes) {
+ int delimitPos = ByteUtils.indexOf(textBytes, (byte) '=');
+
+ // Per https://datatracker.ietf.org/doc/html/rfc6763#section-6.4:
+ // 1. The key MUST be at least one character. DNS-SD TXT record strings
+ // beginning with an '=' character (i.e., the key is missing) MUST be
+ // silently ignored.
+ // 2. If there is no '=' in a DNS-SD TXT record string, then it is a
+ // boolean attribute, simply identified as being present, with no value.
+ if (delimitPos < 0) {
+ return new TextEntry(new String(textBytes, US_ASCII), "");
+ } else if (delimitPos == 0) {
+ return null;
+ }
+ return new TextEntry(
+ new String(Arrays.copyOf(textBytes, delimitPos), US_ASCII),
+ Arrays.copyOfRange(textBytes, delimitPos + 1, textBytes.length));
+ }
+
+ /** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */
+ public TextEntry(String key, String value) {
+ this(key, value.getBytes(UTF_8));
+ }
+
+ /** Creates a new {@link TextEntry} with given key and value of a byte array. */
+ public TextEntry(String key, byte[] value) {
+ this.key = key;
+ this.value = value.clone();
+ }
+
+ private TextEntry(Parcel in) {
+ key = in.readString();
+ value = in.createByteArray();
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public byte[] getValue() {
+ return value.clone();
+ }
+
+ /** Converts this {@link TextEntry} instance to '=' separated byte array. */
+ public byte[] toBytes() {
+ return ByteUtils.concat(key.getBytes(US_ASCII), new byte[]{'='}, value);
+ }
+
+ /** Converts this {@link TextEntry} instance to '=' separated string. */
+ @Override
+ public String toString() {
+ return key + "=" + new String(value, UTF_8);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof TextEntry)) {
+ return false;
+ }
+ TextEntry otherEntry = (TextEntry) other;
+
+ return key.equals(otherEntry.key) && Arrays.equals(value, otherEntry.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * key.hashCode() + Arrays.hashCode(value);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(key);
+ out.writeByteArray(value);
+ }
+ }
}
\ No newline at end of file
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 4fbc809..3747323 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -110,7 +110,8 @@
port,
ipv4Address,
ipv6Address,
- response.getTextRecord().getStrings());
+ response.getTextRecord().getStrings(),
+ response.getTextRecord().getEntries());
}
/**
diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
index a364560..73ecdfa 100644
--- a/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
+++ b/service/mdns/com/android/server/connectivity/mdns/MdnsTextRecord.java
@@ -17,6 +17,7 @@
package com.android.server.connectivity.mdns;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import java.io.IOException;
import java.util.ArrayList;
@@ -24,12 +25,12 @@
import java.util.List;
import java.util.Objects;
-/** An mDNS "TXT" record, which contains a list of text strings. */
+/** An mDNS "TXT" record, which contains a list of {@link TextEntry}. */
// TODO(b/242631897): Resolve nullness suppression.
@SuppressWarnings("nullness")
@VisibleForTesting
public class MdnsTextRecord extends MdnsRecord {
- private List<String> strings;
+ private List<TextEntry> entries;
public MdnsTextRecord(String[] name, MdnsPacketReader reader) throws IOException {
super(name, TYPE_TXT, reader);
@@ -37,22 +38,34 @@
/** Returns the list of strings. */
public List<String> getStrings() {
- return Collections.unmodifiableList(strings);
+ final List<String> list = new ArrayList<>(entries.size());
+ for (TextEntry entry : entries) {
+ list.add(entry.toString());
+ }
+ return Collections.unmodifiableList(list);
+ }
+
+ /** Returns the list of TXT key-value pairs. */
+ public List<TextEntry> getEntries() {
+ return Collections.unmodifiableList(entries);
}
@Override
protected void readData(MdnsPacketReader reader) throws IOException {
- strings = new ArrayList<>();
+ entries = new ArrayList<>();
while (reader.getRemaining() > 0) {
- strings.add(reader.readString());
+ TextEntry entry = reader.readTextEntry();
+ if (entry != null) {
+ entries.add(entry);
+ }
}
}
@Override
protected void writeData(MdnsPacketWriter writer) throws IOException {
- if (strings != null) {
- for (String string : strings) {
- writer.writeString(string);
+ if (entries != null) {
+ for (TextEntry entry : entries) {
+ writer.writeTextEntry(entry);
}
}
}
@@ -61,9 +74,9 @@
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("TXT: {");
- if (strings != null) {
- for (String string : strings) {
- sb.append(' ').append(string);
+ if (entries != null) {
+ for (TextEntry entry : entries) {
+ sb.append(' ').append(entry);
}
}
sb.append("}");
@@ -73,7 +86,7 @@
@Override
public int hashCode() {
- return (super.hashCode() * 31) + Objects.hash(strings);
+ return (super.hashCode() * 31) + Objects.hash(entries);
}
@Override
@@ -85,6 +98,6 @@
return false;
}
- return super.equals(other) && Objects.equals(strings, ((MdnsTextRecord) other).strings);
+ return super.equals(other) && Objects.equals(entries, ((MdnsTextRecord) other).entries);
}
}
\ No newline at end of file
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
index fdb4d4a..9fc4674 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordTests.java
@@ -22,16 +22,19 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import android.util.Log;
import com.android.net.module.util.HexDump;
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.EOFException;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.Inet4Address;
@@ -309,6 +312,14 @@
assertEquals("b=1234567890", strings.get(1));
assertEquals("xyz=!@#$", strings.get(2));
+ List<TextEntry> entries = record.getEntries();
+ assertNotNull(entries);
+ assertEquals(3, entries.size());
+
+ assertEquals(new TextEntry("a", "hello there"), entries.get(0));
+ assertEquals(new TextEntry("b", "1234567890"), entries.get(1));
+ assertEquals(new TextEntry("xyz", "!@#$"), entries.get(2));
+
// Encode
MdnsPacketWriter writer = new MdnsPacketWriter(MAX_PACKET_SIZE);
record.write(writer, record.getReceiptTime());
@@ -321,4 +332,48 @@
assertEquals(dataInText, dataOutText);
}
+
+ @Test
+ public void textRecord_recordDoesNotHaveDataOfGivenLength_throwsEOFException()
+ throws Exception {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "000100001194000D"
+ + "0D613D68656C6C6F" //The TXT entry starts with length of 13, but only 12
+ + "2074686572"); // characters are following it.
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ MdnsRecord.labelsToString(name);
+ reader.readUInt16();
+
+ assertThrows(EOFException.class, () -> new MdnsTextRecord(name, reader));
+ }
+
+ @Test
+ public void textRecord_entriesIncludeNonUtf8Bytes_returnsTheSameUtf8Bytes() throws Exception {
+ final byte[] dataIn = HexDump.hexStringToByteArray(
+ "0474657374000010"
+ + "0001000011940024"
+ + "0D613D68656C6C6F"
+ + "2074686572650C62"
+ + "3D31323334353637"
+ + "3839300878797A3D"
+ + "FFEFDFCF");
+ DatagramPacket packet = new DatagramPacket(dataIn, dataIn.length);
+ MdnsPacketReader reader = new MdnsPacketReader(packet);
+ String[] name = reader.readLabels();
+ MdnsRecord.labelsToString(name);
+ reader.readUInt16();
+
+ MdnsTextRecord record = new MdnsTextRecord(name, reader);
+
+ List<TextEntry> entries = record.getEntries();
+ assertNotNull(entries);
+ assertEquals(3, entries.size());
+ assertEquals(new TextEntry("a", "hello there"), entries.get(0));
+ assertEquals(new TextEntry("b", "1234567890"), entries.get(1));
+ assertEquals(new TextEntry("xyz", HexDump.hexStringToByteArray("FFEFDFCF")),
+ entries.get(2));
+ }
}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
new file mode 100644
index 0000000..79d6046
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceInfoTest.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2022 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.server.connectivity.mdns;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.Map;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public class MdnsServiceInfoTest {
+ @Test
+ public void constructor_createWithOnlyTextStrings_correctAttributes() {
+ MdnsServiceInfo info =
+ new MdnsServiceInfo(
+ "my-mdns-service",
+ new String[] {"_googlecast", "_tcp"},
+ List.of(),
+ new String[] {"my-host", "local"},
+ 12345,
+ "192.168.1.1",
+ "2001::1",
+ List.of("vn=Google Inc.", "mn=Google Nest Hub Max"),
+ /* textEntries= */ null);
+
+ assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
+ assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
+ }
+
+ @Test
+ public void constructor_createWithOnlyTextEntries_correctAttributes() {
+ MdnsServiceInfo info =
+ new MdnsServiceInfo(
+ "my-mdns-service",
+ new String[] {"_googlecast", "_tcp"},
+ List.of(),
+ new String[] {"my-host", "local"},
+ 12345,
+ "192.168.1.1",
+ "2001::1",
+ /* textStrings= */ null,
+ List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
+ MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+
+ assertTrue(info.getAttributeByKey("vn").equals("Google Inc."));
+ assertTrue(info.getAttributeByKey("mn").equals("Google Nest Hub Max"));
+ }
+
+ @Test
+ public void constructor_createWithBothTextStringsAndTextEntries_acceptsOnlyTextEntries() {
+ MdnsServiceInfo info =
+ new MdnsServiceInfo(
+ "my-mdns-service",
+ new String[] {"_googlecast", "_tcp"},
+ List.of(),
+ new String[] {"my-host", "local"},
+ 12345,
+ "192.168.1.1",
+ "2001::1",
+ List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
+ List.of(
+ MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
+ MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+
+ assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
+ info.getAttributes());
+ }
+
+ @Test
+ public void constructor_createWithDuplicateKeys_acceptsTheFirstOne() {
+ MdnsServiceInfo info =
+ new MdnsServiceInfo(
+ "my-mdns-service",
+ new String[] {"_googlecast", "_tcp"},
+ List.of(),
+ new String[] {"my-host", "local"},
+ 12345,
+ "192.168.1.1",
+ "2001::1",
+ List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
+ List.of(MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
+ MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max"),
+ MdnsServiceInfo.TextEntry.fromString("mn=Google WiFi Router")));
+
+ assertEquals(Map.of("vn", "Google Inc.", "mn", "Google Nest Hub Max"),
+ info.getAttributes());
+ }
+
+ @Test
+ public void parcelable_canBeParceledAndUnparceled() {
+ Parcel parcel = Parcel.obtain();
+ MdnsServiceInfo beforeParcel =
+ new MdnsServiceInfo(
+ "my-mdns-service",
+ new String[] {"_googlecast", "_tcp"},
+ List.of(),
+ new String[] {"my-host", "local"},
+ 12345,
+ "192.168.1.1",
+ "2001::1",
+ List.of("vn=Alphabet Inc.", "mn=Google Nest Hub Max", "id=12345"),
+ List.of(
+ MdnsServiceInfo.TextEntry.fromString("vn=Google Inc."),
+ MdnsServiceInfo.TextEntry.fromString("mn=Google Nest Hub Max")));
+
+ beforeParcel.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ MdnsServiceInfo afterParcel = MdnsServiceInfo.CREATOR.createFromParcel(parcel);
+
+ assertEquals(beforeParcel.getServiceInstanceName(), afterParcel.getServiceInstanceName());
+ assertArrayEquals(beforeParcel.getServiceType(), afterParcel.getServiceType());
+ assertEquals(beforeParcel.getSubtypes(), afterParcel.getSubtypes());
+ assertArrayEquals(beforeParcel.getHostName(), afterParcel.getHostName());
+ assertEquals(beforeParcel.getPort(), afterParcel.getPort());
+ assertEquals(beforeParcel.getIpv4Address(), afterParcel.getIpv4Address());
+ assertEquals(beforeParcel.getIpv6Address(), afterParcel.getIpv6Address());
+ assertEquals(beforeParcel.getAttributes(), afterParcel.getAttributes());
+ }
+
+ @Test
+ public void textEntry_parcelable_canBeParceledAndUnparceled() {
+ Parcel parcel = Parcel.obtain();
+ TextEntry beforeParcel = new TextEntry("AA", new byte[] {(byte) 0xFF, (byte) 0xFC});
+
+ beforeParcel.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ TextEntry afterParcel = TextEntry.CREATOR.createFromParcel(parcel);
+
+ assertEquals(beforeParcel, afterParcel);
+ }
+
+ @Test
+ public void textEntry_fromString_keyValueAreExpected() {
+ TextEntry entry = TextEntry.fromString("AA=xxyyzz");
+
+ assertEquals("AA", entry.getKey());
+ assertArrayEquals(new byte[] {'x', 'x', 'y', 'y', 'z', 'z'}, entry.getValue());
+ }
+
+ @Test
+ public void textEntry_fromStringToString_textUnchanged() {
+ TextEntry entry = TextEntry.fromString("AA=xxyyzz");
+
+ assertEquals("AA=xxyyzz", entry.toString());
+ }
+
+ @Test
+ public void textEntry_fromStringWithoutAssignPunc_valueisEmpty() {
+ TextEntry entry = TextEntry.fromString("AA");
+
+ assertEquals("AA", entry.getKey());
+ assertArrayEquals(new byte[] {}, entry.getValue());
+ }
+
+ @Test
+ public void textEntry_fromStringAssignPuncAtBeginning_returnsNull() {
+ TextEntry entry = TextEntry.fromString("=AA");
+
+ assertNull(entry);
+ }
+
+ @Test
+ public void textEntry_fromBytes_keyAndValueAreExpected() {
+ TextEntry entry = TextEntry.fromBytes(
+ new byte[] {'A', 'A', '=', 'x', 'x', 'y', 'y', 'z', 'z'});
+
+ assertEquals("AA", entry.getKey());
+ assertArrayEquals(new byte[] {'x', 'x', 'y', 'y', 'z', 'z'}, entry.getValue());
+ }
+
+ @Test
+ public void textEntry_fromBytesToBytes_textUnchanged() {
+ TextEntry entry = TextEntry.fromBytes(
+ new byte[] {'A', 'A', '=', 'x', 'x', 'y', 'y', 'z', 'z'});
+
+ assertArrayEquals(new byte[] {'A', 'A', '=', 'x', 'x', 'y', 'y', 'z', 'z'},
+ entry.toBytes());
+ }
+
+ @Test
+ public void textEntry_fromBytesWithoutAssignPunc_valueisEmpty() {
+ TextEntry entry = TextEntry.fromBytes(new byte[] {'A', 'A'});
+
+ assertEquals("AA", entry.getKey());
+ assertArrayEquals(new byte[] {}, entry.getValue());
+ }
+
+ @Test
+ public void textEntry_fromBytesAssignPuncAtBeginning_returnsNull() {
+ TextEntry entry = TextEntry.fromBytes(new byte[] {'=', 'A', 'A'});
+
+ assertNull(entry);
+ }
+
+ @Test
+ public void textEntry_fromNonUtf8Bytes_keyValueAreExpected() {
+ TextEntry entry = TextEntry.fromBytes(
+ new byte[] {'A', 'A', '=', (byte) 0xFF, (byte) 0xFE, (byte) 0xFD});
+
+ assertEquals("AA", entry.getKey());
+ assertArrayEquals(new byte[] {(byte) 0xFF, (byte) 0xFE, (byte) 0xFD}, entry.getValue());
+ }
+
+ @Test
+ public void textEntry_equals() {
+ assertEquals(new TextEntry("AA", "xxyyzz"), new TextEntry("AA", "xxyyzz"));
+ assertEquals(new TextEntry("BB", "xxyyzz"), new TextEntry("BB", "xxyyzz"));
+ assertEquals(new TextEntry("AA", "XXYYZZ"), new TextEntry("AA", "XXYYZZ"));
+ }
+}
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
index 5843fd0..c84c386 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsServiceTypeClientTests.java
@@ -32,8 +32,11 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import android.annotation.NonNull;
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry;
import com.android.server.connectivity.mdns.MdnsServiceTypeClient.QueryTaskConfig;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -53,7 +56,6 @@
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.SocketAddress;
-import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -495,7 +497,7 @@
}
@Test
- public void reportExistingServiceToNewlyRegisteredListeners() throws UnknownHostException {
+ public void reportExistingServiceToNewlyRegisteredListeners() throws Exception {
// Process the initial response.
MdnsResponse initialResponse =
createResponse(
@@ -732,7 +734,7 @@
int port,
@NonNull List<String> subtypes,
@NonNull Map<String, String> textAttributes)
- throws UnknownHostException {
+ throws Exception {
String[] hostName = new String[]{"hostname"};
MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class);
when(serviceRecord.getServiceHost()).thenReturn(hostName);
@@ -753,10 +755,13 @@
MdnsTextRecord textRecord = mock(MdnsTextRecord.class);
List<String> textStrings = new ArrayList<>();
+ List<TextEntry> textEntries = new ArrayList<>();
for (Map.Entry<String, String> kv : textAttributes.entrySet()) {
textStrings.add(kv.getKey() + "=" + kv.getValue());
+ textEntries.add(new TextEntry(kv.getKey(), kv.getValue().getBytes(UTF_8)));
}
when(textRecord.getStrings()).thenReturn(textStrings);
+ when(textRecord.getEntries()).thenReturn(textEntries);
response.setServiceRecord(serviceRecord);
response.setTextRecord(textRecord);