diff --git a/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java b/service/mdns/com/android/server/connectivity/mdns/MdnsPacketReader.java index 61c5f5a6bc..856a2cdf91 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 @@ public class MdnsPacketReader { 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 2fed36df94..b78aa5d52e 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 @@ public class MdnsPacketWriter { 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 2e4a4e555b..d14228088a 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 @@ import java.util.Map; * @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 CREATOR = @@ -49,7 +56,8 @@ public class MdnsServiceInfo implements Parcelable { source.readInt(), source.readString(), source.readString(), - source.createStringArrayList()); + source.createStringArrayList(), + source.createTypedArrayList(TextEntry.CREATOR)); } @Override @@ -65,8 +73,33 @@ public class MdnsServiceInfo implements Parcelable { private final int port; private final String ipv4Address; private final String ipv6Address; - private final Map attributes = new HashMap<>(); - List textStrings; + final List textStrings; + @Nullable + final List textEntries; + + private final Map attributes; + + /** Constructs a {@link MdnsServiceInfo} object with default values. */ + public MdnsServiceInfo( + String serviceInstanceName, + String[] serviceType, + List subtypes, + String[] hostName, + int port, + String ipv4Address, + String ipv6Address, + List textStrings) { + this( + serviceInstanceName, + serviceType, + subtypes, + hostName, + port, + ipv4Address, + ipv6Address, + textStrings, + /* textEntries= */ null); + } /** * Constructs a {@link MdnsServiceInfo} object with default values. @@ -81,7 +114,8 @@ public class MdnsServiceInfo implements Parcelable { int port, String ipv4Address, String ipv6Address, - List textStrings) { + List textStrings, + @Nullable List textEntries) { this.serviceInstanceName = serviceInstanceName; this.serviceType = serviceType; this.subtypes = new ArrayList<>(); @@ -92,16 +126,40 @@ public class MdnsServiceInfo implements Parcelable { 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 entries = + (this.textEntries != null) ? this.textEntries : parseTextStrings(this.textStrings); + Map 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 parseTextStrings(List textStrings) { + List 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 @@ public class MdnsServiceInfo implements Parcelable { } /** - * @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 getAttributes() { - return Collections.unmodifiableMap(attributes); + Map map = new HashMap<>(attributes.size()); + for (Map.Entry kv : attributes.entrySet()) { + map.put(kv.getKey(), new String(kv.getValue(), UTF_8)); + } + return Collections.unmodifiableMap(map); } @Override @@ -167,14 +244,6 @@ public class MdnsServiceInfo implements Parcelable { @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 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 @@ public class MdnsServiceInfo implements Parcelable { out.writeString(ipv4Address); out.writeString(ipv6Address); out.writeStringList(textStrings); + out.writeTypedList(textEntries); } @Override @@ -195,4 +265,114 @@ public class MdnsServiceInfo implements Parcelable { 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 CREATOR = + new Parcelable.Creator() { + @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 4fbc809c05..37473239c9 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 @@ public class MdnsServiceTypeClient { 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 a364560976..73ecdfa8f9 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.Collections; 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 strings; + private List entries; public MdnsTextRecord(String[] name, MdnsPacketReader reader) throws IOException { super(name, TYPE_TXT, reader); @@ -37,22 +38,34 @@ public class MdnsTextRecord extends MdnsRecord { /** Returns the list of strings. */ public List getStrings() { - return Collections.unmodifiableList(strings); + final List 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 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 class MdnsTextRecord extends MdnsRecord { 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 @@ public class MdnsTextRecord extends MdnsRecord { @Override public int hashCode() { - return (super.hashCode() * 31) + Objects.hash(strings); + return (super.hashCode() * 31) + Objects.hash(entries); } @Override @@ -85,6 +98,6 @@ public class MdnsTextRecord extends MdnsRecord { 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 fdb4d4acc6..9fc46749ca 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.assertEquals; 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 @@ public class MdnsRecordTests { assertEquals("b=1234567890", strings.get(1)); assertEquals("xyz=!@#$", strings.get(2)); + List 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 @@ public class MdnsRecordTests { 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 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 0000000000..79d60462a4 --- /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 5843fd0ff2..c84c386eaf 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.times; 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.DatagramPacket; 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 @@ public class MdnsServiceTypeClientTests { } @Test - public void reportExistingServiceToNewlyRegisteredListeners() throws UnknownHostException { + public void reportExistingServiceToNewlyRegisteredListeners() throws Exception { // Process the initial response. MdnsResponse initialResponse = createResponse( @@ -732,7 +734,7 @@ public class MdnsServiceTypeClientTests { int port, @NonNull List subtypes, @NonNull Map textAttributes) - throws UnknownHostException { + throws Exception { String[] hostName = new String[]{"hostname"}; MdnsServiceRecord serviceRecord = mock(MdnsServiceRecord.class); when(serviceRecord.getServiceHost()).thenReturn(hostName); @@ -753,10 +755,13 @@ public class MdnsServiceTypeClientTests { MdnsTextRecord textRecord = mock(MdnsTextRecord.class); List textStrings = new ArrayList<>(); + List textEntries = new ArrayList<>(); for (Map.Entry 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);