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 /** Sets `true` to enable SRP server on this device. */ setSrpServerEnabled(boolean enabled)243 public void setSrpServerEnabled(boolean enabled) { 244 String cmd = enabled ? "enable" : "disable"; 245 executeCommand("srp server " + cmd); 246 } 247 248 /** Enables the SRP client and run in autostart mode. */ autoStartSrpClient()249 public void autoStartSrpClient() { 250 executeCommand("srp client autostart enable"); 251 } 252 253 /** Sets the hostname (e.g. "MyHost") for the SRP client. */ setSrpHostname(String hostname)254 public void setSrpHostname(String hostname) { 255 executeCommand("srp client host name " + hostname); 256 } 257 258 /** Sets the host addresses for the SRP client. */ setSrpHostAddresses(List<Inet6Address> addresses)259 public void setSrpHostAddresses(List<Inet6Address> addresses) { 260 executeCommand( 261 "srp client host address " 262 + String.join( 263 " ", 264 addresses.stream().map(Inet6Address::getHostAddress).toList())); 265 } 266 267 /** Removes the SRP host */ removeSrpHost()268 public void removeSrpHost() { 269 executeCommand("srp client host remove 1 1"); 270 } 271 272 /** 273 * Adds an SRP service for the SRP client and wait for the registration to complete. 274 * 275 * @param serviceName the service name like "MyService" 276 * @param serviceType the service type like "_test._tcp" 277 * @param subtypes the service subtypes like "_sub1" 278 * @param port the port number in range [1, 65535] 279 * @param txtMap the map of TXT names and values 280 * @throws TimeoutException if the service isn't registered within timeout 281 */ addSrpService( String serviceName, String serviceType, List<String> subtypes, int port, Map<String, byte[]> txtMap)282 public void addSrpService( 283 String serviceName, 284 String serviceType, 285 List<String> subtypes, 286 int port, 287 Map<String, byte[]> txtMap) 288 throws TimeoutException { 289 StringBuilder fullServiceType = new StringBuilder(serviceType); 290 for (String subtype : subtypes) { 291 fullServiceType.append(",").append(subtype); 292 } 293 waitForSrpServer(); 294 executeCommand( 295 "srp client service add %s %s %d %d %d %s", 296 serviceName, 297 fullServiceType, 298 port, 299 0 /* priority */, 300 0 /* weight */, 301 txtMapToHexString(txtMap)); 302 waitFor(() -> isSrpServiceRegistered(serviceName, serviceType), SERVICE_DISCOVERY_TIMEOUT); 303 } 304 305 /** 306 * Removes an SRP service for the SRP client. 307 * 308 * @param serviceName the service name like "MyService" 309 * @param serviceType the service type like "_test._tcp" 310 * @param notifyServer whether to notify SRP server about the removal 311 */ removeSrpService(String serviceName, String serviceType, boolean notifyServer)312 public void removeSrpService(String serviceName, String serviceType, boolean notifyServer) { 313 String verb = notifyServer ? "remove" : "clear"; 314 executeCommand("srp client service %s %s %s", verb, serviceName, serviceType); 315 } 316 317 /** 318 * Updates an existing SRP service for the SRP client. 319 * 320 * <p>This is essentially a 'remove' and an 'add' on the SRP client's side. 321 * 322 * @param serviceName the service name like "MyService" 323 * @param serviceType the service type like "_test._tcp" 324 * @param subtypes the service subtypes like "_sub1" 325 * @param port the port number in range [1, 65535] 326 * @param txtMap the map of TXT names and values 327 * @throws TimeoutException if the service isn't updated within timeout 328 */ updateSrpService( String serviceName, String serviceType, List<String> subtypes, int port, Map<String, byte[]> txtMap)329 public void updateSrpService( 330 String serviceName, 331 String serviceType, 332 List<String> subtypes, 333 int port, 334 Map<String, byte[]> txtMap) 335 throws TimeoutException { 336 removeSrpService(serviceName, serviceType, false /* notifyServer */); 337 addSrpService(serviceName, serviceType, subtypes, port, txtMap); 338 } 339 340 /** Checks if an SRP service is registered. */ isSrpServiceRegistered(String serviceName, String serviceType)341 public boolean isSrpServiceRegistered(String serviceName, String serviceType) { 342 List<String> lines = executeCommand("srp client service"); 343 for (String line : lines) { 344 if (line.contains(serviceName) && line.contains(serviceType)) { 345 return line.contains("Registered"); 346 } 347 } 348 return false; 349 } 350 351 /** Checks if an SRP host is registered. */ isSrpHostRegistered()352 public boolean isSrpHostRegistered() { 353 List<String> lines = executeCommand("srp client host"); 354 for (String line : lines) { 355 return line.contains("Registered"); 356 } 357 return false; 358 } 359 360 /** Sets the DNS server address. */ setDnsServerAddress(String address)361 public void setDnsServerAddress(String address) { 362 executeCommand("dns config " + address); 363 } 364 365 /** Resolves the {@code queryType} record of the {@code hostname} via DNS. */ resolveHost(String hostname, int queryType)366 public List<InetAddress> resolveHost(String hostname, int queryType) { 367 // CLI output: 368 // DNS response for hostname.com. - fd12::abc1 TTL:50 fd12::abc2 TTL:50 fd12::abc3 TTL:50 369 370 String command; 371 switch (queryType) { 372 case TYPE_A -> command = "resolve4"; 373 case TYPE_AAAA -> command = "resolve"; 374 default -> throw new IllegalArgumentException("Invalid query type: " + queryType); 375 } 376 final List<InetAddress> addresses = new ArrayList<>(); 377 String line; 378 try { 379 line = executeCommand("dns " + command + " " + hostname).get(0); 380 } catch (IllegalStateException e) { 381 return addresses; 382 } 383 final String[] addressTtlPairs = line.split("-")[1].strip().split(" "); 384 for (int i = 0; i < addressTtlPairs.length; i += 2) { 385 addresses.add(InetAddresses.parseNumericAddress(addressTtlPairs[i])); 386 } 387 return addresses; 388 } 389 390 /** Returns the first browsed service instance of {@code serviceType}. */ browseService(String serviceType)391 public NsdServiceInfo browseService(String serviceType) { 392 // CLI output: 393 // DNS browse response for _testservice._tcp.default.service.arpa. 394 // test-service 395 // Port:12345, Priority:0, Weight:0, TTL:10 396 // Host:testhost.default.service.arpa. 397 // HostAddress:2001:0:0:0:0:0:0:1 TTL:10 398 // TXT:[key1=0102, key2=03] TTL:10 399 400 List<String> lines = executeCommand("dns browse " + serviceType); 401 NsdServiceInfo info = new NsdServiceInfo(); 402 info.setServiceName(lines.get(1)); 403 info.setServiceType(serviceType); 404 info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(2))); 405 info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(3))); 406 info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(4)))); 407 DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(5), info); 408 409 return info; 410 } 411 412 /** Returns the resolved service instance. */ resolveService(String serviceName, String serviceType)413 public NsdServiceInfo resolveService(String serviceName, String serviceType) { 414 // CLI output: 415 // DNS service resolution response for test-service for service 416 // _test._tcp.default.service.arpa. 417 // Port:12345, Priority:0, Weight:0, TTL:10 418 // Host:Android.default.service.arpa. 419 // HostAddress:2001:0:0:0:0:0:0:1 TTL:10 420 // TXT:[key1=0102, key2=03] TTL:10 421 422 List<String> lines = executeCommand("dns service %s %s", serviceName, serviceType); 423 NsdServiceInfo info = new NsdServiceInfo(); 424 info.setServiceName(serviceName); 425 info.setServiceType(serviceType); 426 info.setPort(DnsServiceCliOutputParser.parsePort(lines.get(1))); 427 info.setHostname(DnsServiceCliOutputParser.parseHostname(lines.get(2))); 428 info.setHostAddresses(List.of(DnsServiceCliOutputParser.parseHostAddress(lines.get(3)))); 429 DnsServiceCliOutputParser.parseTxtIntoServiceInfo(lines.get(4), info); 430 431 return info; 432 } 433 434 /** Runs the "factoryreset" command on the device. */ factoryReset()435 public void factoryReset() { 436 try { 437 mWriter.write("factoryreset\n"); 438 mWriter.flush(); 439 // fill the input buffer to avoid truncating next command 440 for (int i = 0; i < 1000; ++i) { 441 mWriter.write("\n"); 442 } 443 mWriter.flush(); 444 } catch (IOException e) { 445 throw new IllegalStateException("Failed to run factoryreset on ot-cli-ftd", e); 446 } 447 } 448 subscribeMulticastAddress(Inet6Address address)449 public void subscribeMulticastAddress(Inet6Address address) { 450 executeCommand("ipmaddr add " + address.getHostAddress()); 451 } 452 ping(InetAddress address, Inet6Address source)453 public void ping(InetAddress address, Inet6Address source) { 454 ping( 455 address, 456 source, 457 PING_SIZE, 458 1 /* count */, 459 PING_INTERVAL, 460 HOP_LIMIT, 461 PING_TIMEOUT_0_1_SECOND); 462 } 463 ping(InetAddress address)464 public void ping(InetAddress address) { 465 ping( 466 address, 467 null, 468 PING_SIZE, 469 1 /* count */, 470 PING_INTERVAL, 471 HOP_LIMIT, 472 PING_TIMEOUT_0_1_SECOND); 473 } 474 475 /** Returns the number of ping reply packets received. */ ping(InetAddress address, int count)476 public int ping(InetAddress address, int count) { 477 List<String> output = 478 ping( 479 address, 480 null, 481 PING_SIZE, 482 count, 483 PING_INTERVAL, 484 HOP_LIMIT, 485 PING_TIMEOUT_1_SECOND); 486 return getReceivedPacketsCount(output); 487 } 488 ping( InetAddress address, Inet6Address source, int size, int count, int interval, int hopLimit, float timeout)489 private List<String> ping( 490 InetAddress address, 491 Inet6Address source, 492 int size, 493 int count, 494 int interval, 495 int hopLimit, 496 float timeout) { 497 String cmd = 498 "ping" 499 + ((source == null) ? "" : (" -I " + source.getHostAddress())) 500 + " " 501 + address.getHostAddress() 502 + " " 503 + size 504 + " " 505 + count 506 + " " 507 + interval 508 + " " 509 + hopLimit 510 + " " 511 + timeout; 512 return executeCommand(cmd); 513 } 514 getReceivedPacketsCount(List<String> stringList)515 private int getReceivedPacketsCount(List<String> stringList) { 516 Pattern pattern = Pattern.compile("([\\d]+) packets received"); 517 518 for (String message : stringList) { 519 Matcher matcher = pattern.matcher(message); 520 if (matcher.find()) { 521 String packetCountStr = matcher.group(1); 522 return Integer.parseInt(packetCountStr); 523 } 524 } 525 // No match found 526 return -1; 527 } 528 529 /** Waits for an SRP server to be present in Network Data */ waitForSrpServer()530 public void waitForSrpServer() throws TimeoutException { 531 // CLI output: 532 // > srp client server 533 // [fd64:db12:25f4:7e0b:1bfc:6344:25ac:2dd7]:53538 534 // Done 535 waitFor( 536 () -> { 537 final String serverAddr = executeCommand("srp client server").get(0); 538 final int lastColonIndex = serverAddr.lastIndexOf(':'); 539 final int port = Integer.parseInt(serverAddr.substring(lastColonIndex + 1)); 540 return port > 0; 541 }, 542 SERVICE_DISCOVERY_TIMEOUT); 543 } 544 545 @FormatMethod executeCommand(String commandFormat, Object... args)546 private List<String> executeCommand(String commandFormat, Object... args) { 547 return executeCommand(String.format(commandFormat, args)); 548 } 549 executeCommand(String command)550 private List<String> executeCommand(String command) { 551 try { 552 mWriter.write(command + "\n"); 553 mWriter.flush(); 554 } catch (IOException e) { 555 throw new IllegalStateException( 556 "Failed to write the command " + command + " to ot-cli-ftd", e); 557 } 558 try { 559 return readUntilDone(); 560 } catch (IOException e) { 561 throw new IllegalStateException( 562 "Failed to read the ot-cli-ftd output of command: " + command, e); 563 } 564 } 565 readLine()566 private String readLine() throws IOException { 567 final CompletableFuture<String> future = new CompletableFuture<>(); 568 mReaderHandler.post( 569 () -> { 570 try { 571 future.complete(mReader.readLine()); 572 } catch (IOException e) { 573 future.completeExceptionally(e); 574 } 575 }); 576 try { 577 return future.get(READ_LINE_TIMEOUT_SECONDS, SECONDS); 578 } catch (InterruptedException | ExecutionException | TimeoutException e) { 579 throw new IOException("Failed to read a line from ot-cli-ftd"); 580 } 581 } 582 readUntilDone()583 private List<String> readUntilDone() throws IOException { 584 ArrayList<String> result = new ArrayList<>(); 585 String line; 586 while ((line = readLine()) != null) { 587 if (line.equals("Done")) { 588 break; 589 } 590 if (line.startsWith("Error")) { 591 throw new IOException("ot-cli-ftd reported an error: " + line); 592 } 593 if (!line.startsWith("> ")) { 594 result.add(line); 595 } 596 } 597 return result; 598 } 599 txtMapToHexString(Map<String, byte[]> txtMap)600 private static String txtMapToHexString(Map<String, byte[]> txtMap) { 601 if (txtMap == null) { 602 return ""; 603 } 604 StringBuilder sb = new StringBuilder(); 605 for (Map.Entry<String, byte[]> entry : txtMap.entrySet()) { 606 int length = entry.getKey().length() + entry.getValue().length + 1; 607 sb.append(String.format("%02x", length)); 608 sb.append(toHexString(entry.getKey())); 609 sb.append(toHexString("=")); 610 sb.append(toHexString(entry.getValue())); 611 } 612 return sb.toString(); 613 } 614 toHexString(String s)615 private static String toHexString(String s) { 616 return toHexString(s.getBytes(StandardCharsets.UTF_8)); 617 } 618 toHexString(byte[] bytes)619 private static String toHexString(byte[] bytes) { 620 return base16().encode(bytes); 621 } 622 623 private static final class DnsServiceCliOutputParser { 624 /** Returns the first match in the input of a given regex pattern. */ firstMatchOf(String input, String regex)625 private static Matcher firstMatchOf(String input, String regex) { 626 Matcher matcher = Pattern.compile(regex).matcher(input); 627 matcher.find(); 628 return matcher; 629 } 630 631 // Example: "Port:12345" parsePort(String line)632 private static int parsePort(String line) { 633 return Integer.parseInt(firstMatchOf(line, "Port:(\\d+)").group(1)); 634 } 635 636 // Example: "Host:Android.default.service.arpa." parseHostname(String line)637 private static String parseHostname(String line) { 638 return firstMatchOf(line, "Host:(.+)").group(1); 639 } 640 641 // Example: "HostAddress:2001:0:0:0:0:0:0:1" parseHostAddress(String line)642 private static InetAddress parseHostAddress(String line) { 643 return InetAddresses.parseNumericAddress( 644 firstMatchOf(line, "HostAddress:([^ ]+)").group(1)); 645 } 646 647 // Example: "TXT:[key1=0102, key2=03]" parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo)648 private static void parseTxtIntoServiceInfo(String line, NsdServiceInfo serviceInfo) { 649 String txtString = firstMatchOf(line, "TXT:\\[([^\\]]+)\\]").group(1); 650 for (String txtEntry : txtString.split(",")) { 651 String[] nameAndValue = txtEntry.trim().split("="); 652 String name = nameAndValue[0]; 653 String value = nameAndValue[1]; 654 byte[] bytes = new byte[value.length() / 2]; 655 for (int i = 0; i < value.length(); i += 2) { 656 byte b = (byte) ((value.charAt(i) - '0') << 4 | (value.charAt(i + 1) - '0')); 657 bytes[i / 2] = b; 658 } 659 serviceInfo.setAttribute(name, bytes); 660 } 661 } 662 } 663 } 664