1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.net.thread.utils;
17 
18 import static android.net.DnsResolver.TYPE_A;
19 import static android.net.DnsResolver.TYPE_AAAA;
20 import static android.net.thread.utils.IntegrationTestUtils.SERVICE_DISCOVERY_TIMEOUT;
21 import static android.net.thread.utils.IntegrationTestUtils.waitFor;
22 
23 import static com.google.common.io.BaseEncoding.base16;
24 
25 import static java.util.concurrent.TimeUnit.SECONDS;
26 
27 import android.net.InetAddresses;
28 import android.net.IpPrefix;
29 import android.net.nsd.NsdServiceInfo;
30 import android.net.thread.ActiveOperationalDataset;
31 import android.os.Handler;
32 import android.os.HandlerThread;
33 
34 import com.google.errorprone.annotations.FormatMethod;
35 
36 import java.io.BufferedReader;
37 import java.io.BufferedWriter;
38 import java.io.IOException;
39 import java.io.InputStreamReader;
40 import java.io.OutputStreamWriter;
41 import java.net.Inet6Address;
42 import java.net.InetAddress;
43 import java.nio.charset.StandardCharsets;
44 import java.time.Duration;
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.concurrent.CompletableFuture;
49 import java.util.concurrent.ExecutionException;
50 import java.util.concurrent.TimeoutException;
51 import java.util.regex.Matcher;
52 import java.util.regex.Pattern;
53 
54 /**
55  * A class that launches and controls a simulation Full Thread Device (FTD).
56  *
57  * <p>This class launches an `ot-cli-ftd` process and communicates with it via command line input
58  * and output. See <a
59  * href="https://github.com/openthread/openthread/blob/main/src/cli/README.md">this page</a> for
60  * available commands.
61  */
62 public final class FullThreadDevice {
63     private static final int HOP_LIMIT = 64;
64     private static final int PING_INTERVAL = 1;
65     private static final int PING_SIZE = 100;
66     // There may not be a response for the ping command, using a short timeout to keep the tests
67     // short.
68     private static final float PING_TIMEOUT_0_1_SECOND = 0.1f;
69     // 1 second timeout should be used when response is expected.
70     private static final float PING_TIMEOUT_1_SECOND = 1f;
71     private static final int READ_LINE_TIMEOUT_SECONDS = 5;
72 
73     private final Process mProcess;
74     private final BufferedReader mReader;
75     private final BufferedWriter mWriter;
76     private final HandlerThread mReaderHandlerThread;
77     private final Handler mReaderHandler;
78 
79     private ActiveOperationalDataset mActiveOperationalDataset;
80 
81     /**
82      * Constructs a {@link FullThreadDevice} for the given node ID.
83      *
84      * <p>It launches an `ot-cli-ftd` process using the given node ID. The node ID is an integer in
85      * range [1, OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE]. `OPENTHREAD_SIMULATION_MAX_NETWORK_SIZE`
86      * is defined in `external/openthread/examples/platforms/simulation/platform-config.h`.
87      *
88      * @param nodeId the node ID for the simulation Full Thread Device.
89      * @throws IllegalStateException the node ID is already occupied by another simulation Thread
90      *     device.
91      */
FullThreadDevice(int nodeId)92     public FullThreadDevice(int nodeId) {
93         try {
94             mProcess = Runtime.getRuntime().exec("/system/bin/ot-cli-ftd -Leth1 " + nodeId);
95         } catch (IOException e) {
96             throw new IllegalStateException(
97                     "Failed to start ot-cli-ftd -Leth1 (id=" + nodeId + ")", e);
98         }
99         mReader = new BufferedReader(new InputStreamReader(mProcess.getInputStream()));
100         mWriter = new BufferedWriter(new OutputStreamWriter(mProcess.getOutputStream()));
101         mReaderHandlerThread = new HandlerThread("FullThreadDeviceReader");
102         mReaderHandlerThread.start();
103         mReaderHandler = new Handler(mReaderHandlerThread.getLooper());
104         mActiveOperationalDataset = null;
105     }
106 
destroy()107     public void destroy() {
108         mProcess.destroy();
109         mReaderHandlerThread.quit();
110     }
111 
112     /**
113      * Returns an OMR (Off-Mesh-Routable) address on this device if any.
114      *
115      * <p>This methods goes through all unicast addresses on the device and returns the first
116      * address which is neither link-local nor mesh-local.
117      */
getOmrAddress()118     public Inet6Address getOmrAddress() {
119         List<String> addresses = executeCommand("ipaddr");
120         IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
121         for (String address : addresses) {
122             if (address.startsWith("fe80:")) {
123                 continue;
124             }
125             Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
126             if (!meshLocalPrefix.contains(addr)) {
127                 return addr;
128             }
129         }
130         return null;
131     }
132 
133     /** Returns the Mesh-local EID address on this device if any. */
getMlEid()134     public Inet6Address getMlEid() {
135         List<String> addresses = executeCommand("ipaddr mleid");
136         return (Inet6Address) InetAddresses.parseNumericAddress(addresses.get(0));
137     }
138 
139     /**
140      * Returns the link-local address of the device.
141      *
142      * <p>This methods goes through all unicast addresses on the device and returns the address that
143      * begins with fe80.
144      */
getLinkLocalAddress()145     public Inet6Address getLinkLocalAddress() {
146         List<String> output = executeCommand("ipaddr linklocal");
147         if (!output.isEmpty() && output.get(0).startsWith("fe80:")) {
148             return (Inet6Address) InetAddresses.parseNumericAddress(output.get(0));
149         }
150         return null;
151     }
152 
153     /**
154      * Returns the mesh-local addresses of the device.
155      *
156      * <p>This methods goes through all unicast addresses on the device and returns the address that
157      * begins with mesh-local prefix.
158      */
getMeshLocalAddresses()159     public List<Inet6Address> getMeshLocalAddresses() {
160         List<String> addresses = executeCommand("ipaddr");
161         List<Inet6Address> meshLocalAddresses = new ArrayList<>();
162         IpPrefix meshLocalPrefix = mActiveOperationalDataset.getMeshLocalPrefix();
163         for (String address : addresses) {
164             Inet6Address addr = (Inet6Address) InetAddresses.parseNumericAddress(address);
165             if (meshLocalPrefix.contains(addr)) {
166                 meshLocalAddresses.add(addr);
167             }
168         }
169         return meshLocalAddresses;
170     }
171 
172     /**
173      * Joins the Thread network using the given {@link ActiveOperationalDataset}.
174      *
175      * @param dataset the Active Operational Dataset
176      */
joinNetwork(ActiveOperationalDataset dataset)177     public void joinNetwork(ActiveOperationalDataset dataset) {
178         mActiveOperationalDataset = dataset;
179         executeCommand("dataset set active " + base16().lowerCase().encode(dataset.toThreadTlvs()));
180         executeCommand("ifconfig up");
181         executeCommand("thread start");
182     }
183 
184     /** Stops the Thread network radio. */
stopThreadRadio()185     public void stopThreadRadio() {
186         executeCommand("thread stop");
187         executeCommand("ifconfig down");
188     }
189 
190     /**
191      * Waits for the Thread device to enter the any state of the given {@link List<String>}.
192      *
193      * @param states the list of states to wait for. Valid states are "disabled", "detached",
194      *     "child", "router" and "leader".
195      * @param timeout the time to wait for the expected state before throwing
196      */
waitForStateAnyOf(List<String> states, Duration timeout)197     public void waitForStateAnyOf(List<String> states, Duration timeout) throws TimeoutException {
198         waitFor(() -> states.contains(getState()), timeout);
199     }
200 
201     /**
202      * Gets the state of the Thread device.
203      *
204      * @return a string representing the state.
205      */
getState()206     public String getState() {
207         return executeCommand("state").get(0);
208     }
209 
210     /** Closes the UDP socket. */
udpClose()211     public void udpClose() {
212         executeCommand("udp close");
213     }
214 
215     /** Opens the UDP socket. */
udpOpen()216     public void udpOpen() {
217         executeCommand("udp open");
218     }
219 
220     /** Opens the UDP socket and binds it to a specific address and port. */
udpBind(Inet6Address address, int port)221     public void udpBind(Inet6Address address, int port) {
222         udpClose();
223         udpOpen();
224         executeCommand("udp bind %s %d", address.getHostAddress(), port);
225     }
226 
227     /** Returns the message received on the UDP socket. */
udpReceive()228     public String udpReceive() throws IOException {
229         Pattern pattern =
230                 Pattern.compile("> (\\d+) bytes from ([\\da-f:]+) (\\d+) ([\\x00-\\x7F]+)");
231         Matcher matcher = pattern.matcher(readLine());
232         matcher.matches();
233 
234         return matcher.group(4);
235     }
236 
237     /** Sends a UDP message to given IP address and port. */
udpSend(String message, InetAddress serverAddr, int serverPort)238     public void udpSend(String message, InetAddress serverAddr, int serverPort) {
239         executeCommand("udp send %s %d %s", serverAddr.getHostAddress(), serverPort, message);
240     }
241 
242     /** Enables the SRP client and run in autostart mode. */
autoStartSrpClient()243     public void autoStartSrpClient() {
244         executeCommand("srp client autostart enable");
245     }
246 
247     /** Sets the hostname (e.g. "MyHost") for the SRP client. */
setSrpHostname(String hostname)248     public void setSrpHostname(String hostname) {
249         executeCommand("srp client host name " + hostname);
250     }
251 
252     /** Sets the host addresses for the SRP client. */
setSrpHostAddresses(List<Inet6Address> addresses)253     public void setSrpHostAddresses(List<Inet6Address> addresses) {
254         executeCommand(
255                 "srp client host address "
256                         + String.join(
257                                 " ",
258                                 addresses.stream().map(Inet6Address::getHostAddress).toList()));
259     }
260 
261     /** Removes the SRP host */
removeSrpHost()262     public void removeSrpHost() {
263         executeCommand("srp client host remove 1 1");
264     }
265 
266     /**
267      * Adds an SRP service for the SRP client and wait for the registration to complete.
268      *
269      * @param serviceName the service name like "MyService"
270      * @param serviceType the service type like "_test._tcp"
271      * @param subtypes the service subtypes like "_sub1"
272      * @param port the port number in range [1, 65535]
273      * @param txtMap the map of TXT names and values
274      * @throws TimeoutException if the service isn't registered within timeout
275      */
addSrpService( String serviceName, String serviceType, List<String> subtypes, int port, Map<String, byte[]> txtMap)276     public void addSrpService(
277             String serviceName,
278             String serviceType,
279             List<String> subtypes,
280             int port,
281             Map<String, byte[]> txtMap)
282             throws TimeoutException {
283         StringBuilder fullServiceType = new StringBuilder(serviceType);
284         for (String subtype : subtypes) {
285             fullServiceType.append(",").append(subtype);
286         }
287         waitForSrpServer();
288         executeCommand(
289                 "srp client service add %s %s %d %d %d %s",
290                 serviceName,
291                 fullServiceType,
292                 port,
293                 0 /* priority */,
294                 0 /* weight */,
295                 txtMapToHexString(txtMap));
296         waitFor(() -> isSrpServiceRegistered(serviceName, serviceType), SERVICE_DISCOVERY_TIMEOUT);
297     }
298 
299     /**
300      * Removes an SRP service for the SRP client.
301      *
302      * @param serviceName the service name like "MyService"
303      * @param serviceType the service type like "_test._tcp"
304      * @param notifyServer whether to notify SRP server about the removal
305      */
removeSrpService(String serviceName, String serviceType, boolean notifyServer)306     public void removeSrpService(String serviceName, String serviceType, boolean notifyServer) {
307         String verb = notifyServer ? "remove" : "clear";
308         executeCommand("srp client service %s %s %s", verb, serviceName, serviceType);
309     }
310 
311     /**
312      * Updates an existing SRP service for the SRP client.
313      *
314      * <p>This is essentially a 'remove' and an 'add' on the SRP client's side.
315      *
316      * @param serviceName the service name like "MyService"
317      * @param serviceType the service type like "_test._tcp"
318      * @param subtypes the service subtypes like "_sub1"
319      * @param port the port number in range [1, 65535]
320      * @param txtMap the map of TXT names and values
321      * @throws TimeoutException if the service isn't updated within timeout
322      */
updateSrpService( String serviceName, String serviceType, List<String> subtypes, int port, Map<String, byte[]> txtMap)323     public void updateSrpService(
324             String serviceName,
325             String serviceType,
326             List<String> subtypes,
327             int port,
328             Map<String, byte[]> txtMap)
329             throws TimeoutException {
330         removeSrpService(serviceName, serviceType, false /* notifyServer */);
331         addSrpService(serviceName, serviceType, subtypes, port, txtMap);
332     }
333 
334     /** Checks if an SRP service is registered. */
isSrpServiceRegistered(String serviceName, String serviceType)335     public boolean isSrpServiceRegistered(String serviceName, String serviceType) {
336         List<String> lines = executeCommand("srp client service");
337         for (String line : lines) {
338             if (line.contains(serviceName) && line.contains(serviceType)) {
339                 return line.contains("Registered");
340             }
341         }
342         return false;
343     }
344 
345     /** Checks if an SRP host is registered. */
isSrpHostRegistered()346     public boolean isSrpHostRegistered() {
347         List<String> lines = executeCommand("srp client host");
348         for (String line : lines) {
349             return line.contains("Registered");
350         }
351         return false;
352     }
353 
354     /** Sets the DNS server address. */
setDnsServerAddress(String address)355     public void setDnsServerAddress(String address) {
356         executeCommand("dns config " + address);
357     }
358 
359     /** Resolves the {@code queryType} record of the {@code hostname} via DNS. */
resolveHost(String hostname, int queryType)360     public List<InetAddress> resolveHost(String hostname, int queryType) {
361         // CLI output:
362         // DNS response for hostname.com. - fd12::abc1 TTL:50 fd12::abc2 TTL:50 fd12::abc3 TTL:50
363 
364         String command;
365         switch (queryType) {
366             case TYPE_A -> command = "resolve4";
367             case TYPE_AAAA -> command = "resolve";
368             default -> throw new IllegalArgumentException("Invalid query type: " + queryType);
369         }
370         final List<InetAddress> addresses = new ArrayList<>();
371         String line;
372         try {
373             line = executeCommand("dns " + command + " " + hostname).get(0);
374         } catch (IllegalStateException e) {
375             return addresses;
376         }
377         final String[] addressTtlPairs = line.split("-")[1].strip().split(" ");
378         for (int i = 0; i < addressTtlPairs.length; i += 2) {
379             addresses.add(InetAddresses.parseNumericAddress(addressTtlPairs[i]));
380         }
381         return addresses;
382     }
383 
384     /** Returns the first browsed service instance of {@code serviceType}. */
browseService(String serviceType)385     public NsdServiceInfo browseService(String serviceType) {
386         // CLI output:
387         // DNS browse response for _testservice._tcp.default.service.arpa.
388         // test-service
389         //    Port:12345, Priority:0, Weight:0, TTL:10
390         //    Host:testhost.default.service.arpa.
391         //    HostAddress:2001:0:0:0:0:0:0:1 TTL:10
392         //    TXT:[key1=0102, key2=03] TTL:10
393 
394         List<String> lines = executeCommand("dns browse " + serviceType);
395         NsdServiceInfo info = new NsdServiceInfo();
396         info.setServiceName(lines.get(1));
397         info.setServiceType(serviceType);
398         info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(2)));
399         info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(3)));
400         info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(4))));
401         DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(5), info);
402 
403         return info;
404     }
405 
406     /** Returns the resolved service instance. */
resolveService(String serviceName, String serviceType)407     public NsdServiceInfo resolveService(String serviceName, String serviceType) {
408         // CLI output:
409         // DNS service resolution response for test-service for service
410         // _test._tcp.default.service.arpa.
411         // Port:12345, Priority:0, Weight:0, TTL:10
412         // Host:Android.default.service.arpa.
413         // HostAddress:2001:0:0:0:0:0:0:1 TTL:10
414         // TXT:[key1=0102, key2=03] TTL:10
415 
416         List<String> lines = executeCommand("dns service %s %s", serviceName, serviceType);
417         NsdServiceInfo info = new NsdServiceInfo();
418         info.setServiceName(serviceName);
419         info.setServiceType(serviceType);
420         info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(1)));
421         info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(2)));
422         info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(3))));
423         DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(4), info);
424 
425         return info;
426     }
427 
428     /** Runs the "factoryreset" command on the device. */
factoryReset()429     public void factoryReset() {
430         try {
431             mWriter.write("factoryreset\n");
432             mWriter.flush();
433             // fill the input buffer to avoid truncating next command
434             for (int i = 0; i < 1000; ++i) {
435                 mWriter.write("\n");
436             }
437             mWriter.flush();
438         } catch (IOException e) {
439             throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e);
440         }
441     }
442 
subscribeMulticastAddress(Inet6Address address)443     public void subscribeMulticastAddress(Inet6Address address) {
444         executeCommand("ipmaddr add " + address.getHostAddress());
445     }
446 
ping(InetAddress address, Inet6Address source)447     public void ping(InetAddress address, Inet6Address source) {
448         ping(
449                 address,
450                 source,
451                 PING_SIZE,
452                 1 /* count */,
453                 PING_INTERVAL,
454                 HOP_LIMIT,
455                 PING_TIMEOUT_0_1_SECOND);
456     }
457 
ping(InetAddress address)458     public void ping(InetAddress address) {
459         ping(
460                 address,
461                 null,
462                 PING_SIZE,
463                 1 /* count */,
464                 PING_INTERVAL,
465                 HOP_LIMIT,
466                 PING_TIMEOUT_0_1_SECOND);
467     }
468 
469     /** Returns the number of ping reply packets received. */
ping(InetAddress address, int count)470     public int ping(InetAddress address, int count) {
471         List<String> output =
472                 ping(
473                         address,
474                         null,
475                         PING_SIZE,
476                         count,
477                         PING_INTERVAL,
478                         HOP_LIMIT,
479                         PING_TIMEOUT_1_SECOND);
480         return getReceivedPacketsCount(output);
481     }
482 
ping( InetAddress address, Inet6Address source, int size, int count, int interval, int hopLimit, float timeout)483     private List<String> ping(
484             InetAddress address,
485             Inet6Address source,
486             int size,
487             int count,
488             int interval,
489             int hopLimit,
490             float timeout) {
491         String cmd =
492                 "ping"
493                         + ((source == null) ? "" : (" -I " + source.getHostAddress()))
494                         + " "
495                         + address.getHostAddress()
496                         + " "
497                         + size
498                         + " "
499                         + count
500                         + " "
501                         + interval
502                         + " "
503                         + hopLimit
504                         + " "
505                         + timeout;
506         return executeCommand(cmd);
507     }
508 
getReceivedPacketsCount(List<String> stringList)509     private int getReceivedPacketsCount(List<String> stringList) {
510         Pattern pattern = Pattern.compile("([\\d]+) packets received");
511 
512         for (String message : stringList) {
513             Matcher matcher = pattern.matcher(message);
514             if (matcher.find()) {
515                 String packetCountStr = matcher.group(1);
516                 return Integer.parseInt(packetCountStr);
517             }
518         }
519         // No match found
520         return -1;
521     }
522 
523     /** Waits for an SRP server to be present in Network Data */
waitForSrpServer()524     private void waitForSrpServer() throws TimeoutException {
525         // CLI output:
526         // > srp client server
527         // [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538
528         // Done
529         waitFor(
530                 () -> {
531                     final String serverAddr = executeCommand("srp client server").get(0);
532                     final int lastColonIndex = serverAddr.lastIndexOf(':');
533                     final int port = Integer.parseInt(serverAddr.substring(lastColonIndex + 1));
534                     return port > 0;
535                 },
536                 SERVICE_DISCOVERY_TIMEOUT);
537     }
538 
539     @FormatMethod
executeCommand(String commandFormat, Object... args)540     private List<String> executeCommand(String commandFormat, Object... args) {
541         return executeCommand(String.format(commandFormat, args));
542     }
543 
executeCommand(String command)544     private List<String> executeCommand(String command) {
545         try {
546             mWriter.write(command + "\n");
547             mWriter.flush();
548         } catch (IOException e) {
549             throw new IllegalStateException(
550                     "Failed to write the command " + command + " to ot-cli-ftd", e);
551         }
552         try {
553             return readUntilDone();
554         } catch (IOException e) {
555             throw new IllegalStateException(
556                     "Failed to read the ot-cli-ftd output of command: " + command, e);
557         }
558     }
559 
readLine()560     private String readLine() throws IOException {
561         final CompletableFuture<String> future = new CompletableFuture<>();
562         mReaderHandler.post(
563                 () -> {
564                     try {
565                         future.complete(mReader.readLine());
566                     } catch (IOException e) {
567                         future.completeExceptionally(e);
568                     }
569                 });
570         try {
571             return future.get(READ_LINE_TIMEOUT_SECONDS, SECONDS);
572         } catch (InterruptedException | ExecutionException | TimeoutException e) {
573             throw new IOException("Failed to read a line from ot-cli-ftd");
574         }
575     }
576 
readUntilDone()577     private List<String> readUntilDone() throws IOException {
578         ArrayList<String> result = new ArrayList<>();
579         String line;
580         while ((line = readLine()) != null) {
581             if (line.equals("Done")) {
582                 break;
583             }
584             if (line.startsWith("Error")) {
585                 throw new IOException("ot-cli-ftd reported an error: " + line);
586             }
587             if (!line.startsWith("> ")) {
588                 result.add(line);
589             }
590         }
591         return result;
592     }
593 
txtMapToHexString(Map<String, byte[]> txtMap)594     private static String txtMapToHexString(Map<String, byte[]> txtMap) {
595         if (txtMap == null) {
596             return "";
597         }
598         StringBuilder sb = new StringBuilder();
599         for (Map.Entry<String, byte[]> entry : txtMap.entrySet()) {
600             int length = entry.getKey().length() + entry.getValue().length + 1;
601             sb.append(String.format("%02x", length));
602             sb.append(toHexString(entry.getKey()));
603             sb.append(toHexString("="));
604             sb.append(toHexString(entry.getValue()));
605         }
606         return sb.toString();
607     }
608 
toHexString(String s)609     private static String toHexString(String s) {
610         return toHexString(s.getBytes(StandardCharsets.UTF_8));
611     }
612 
toHexString(byte[] bytes)613     private static String toHexString(byte[] bytes) {
614         return base16().encode(bytes);
615     }
616 
617     private static final class DnsServiceCliOutputParser {
618         /** Returns the first match in the input of a given regex pattern. */
firstMatchOf(String input, String regex)619         private static Matcher firstMatchOf(String input, String regex) {
620             Matcher matcher = Pattern.compile(regex).matcher(input);
621             matcher.find();
622             return matcher;
623         }
624 
625         // Example: "Port:12345"
parsePort(String line)626         private static int parsePort(String line) {
627             return Integer.parseInt(firstMatchOf(line, "Port:(\\d+)").group(1));
628         }
629 
630         // Example: "Host:Android.default.service.arpa."
parseHostname(String line)631         private static String parseHostname(String line) {
632             return firstMatchOf(line, "Host:(.+)").group(1);
633         }
634 
635         // Example: "HostAddress:2001:0:0:0:0:0:0:1"
parseHostAddress(String line)636         private static InetAddress parseHostAddress(String line) {
637             return InetAddresses.parseNumericAddress(
638                     firstMatchOf(line, "HostAddress:([^ ]+)").group(1));
639         }
640 
641         // Example: "TXT:[key1=0102, key2=03]"
parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo)642         private static void parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo) {
643             String txtString = firstMatchOf(line, "TXT:\\[([^\\]]+)\\]").group(1);
644             for (String txtEntry : txtString.split(",")) {
645                 String[] nameAndValue = txtEntry.trim().split("=");
646                 String name = nameAndValue[0];
647                 String value = nameAndValue[1];
648                 byte[] bytes = new byte[value.length() / 2];
649                 for (int i = 0; i < value.length(); i += 2) {
650                     byte b = (byte) ((value.charAt(i) - '0') << 4 | (value.charAt(i + 1) - '0'));
651                     bytes[i / 2] = b;
652                 }
653                 serviceInfo.setAttribute(name, bytes);
654             }
655         }
656     }
657 }
658