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 17 package com.android.server.connectivity.mdns.util; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.net.Network; 22 import android.os.Handler; 23 24 import com.android.server.connectivity.mdns.MdnsConstants; 25 import com.android.server.connectivity.mdns.MdnsRecord; 26 27 import java.nio.ByteBuffer; 28 import java.nio.CharBuffer; 29 import java.nio.charset.Charset; 30 import java.nio.charset.CharsetEncoder; 31 import java.nio.charset.StandardCharsets; 32 33 /** 34 * Mdns utility functions. 35 */ 36 public class MdnsUtils { 37 MdnsUtils()38 private MdnsUtils() { } 39 40 /** 41 * Convert the string to DNS case-insensitive lowercase 42 * 43 * Per rfc6762#page-46, accented characters are not defined to be automatically equivalent to 44 * their unaccented counterparts. So the "DNS lowercase" should be if character is A-Z then they 45 * transform into a-z. Otherwise, they are kept as-is. 46 */ toDnsLowerCase(@onNull String string)47 public static String toDnsLowerCase(@NonNull String string) { 48 final char[] outChars = new char[string.length()]; 49 for (int i = 0; i < string.length(); i++) { 50 outChars[i] = toDnsLowerCase(string.charAt(i)); 51 } 52 return new String(outChars); 53 } 54 55 /** 56 * Compare two strings by DNS case-insensitive lowercase. 57 */ equalsIgnoreDnsCase(@onNull String a, @NonNull String b)58 public static boolean equalsIgnoreDnsCase(@NonNull String a, @NonNull String b) { 59 if (a.length() != b.length()) return false; 60 for (int i = 0; i < a.length(); i++) { 61 if (toDnsLowerCase(a.charAt(i)) != toDnsLowerCase(b.charAt(i))) { 62 return false; 63 } 64 } 65 return true; 66 } 67 toDnsLowerCase(char a)68 private static char toDnsLowerCase(char a) { 69 return a >= 'A' && a <= 'Z' ? (char) (a + ('a' - 'A')) : a; 70 } 71 72 /*** Ensure that current running thread is same as given handler thread */ ensureRunningOnHandlerThread(@onNull Handler handler)73 public static void ensureRunningOnHandlerThread(@NonNull Handler handler) { 74 if (!isRunningOnHandlerThread(handler)) { 75 throw new IllegalStateException( 76 "Not running on Handler thread: " + Thread.currentThread().getName()); 77 } 78 } 79 80 /*** Check that current running thread is same as given handler thread */ isRunningOnHandlerThread(@onNull Handler handler)81 public static boolean isRunningOnHandlerThread(@NonNull Handler handler) { 82 if (handler.getLooper().getThread() == Thread.currentThread()) { 83 return true; 84 } 85 return false; 86 } 87 88 /*** Check whether the target network is matched current network */ isNetworkMatched(@ullable Network targetNetwork, @Nullable Network currentNetwork)89 public static boolean isNetworkMatched(@Nullable Network targetNetwork, 90 @Nullable Network currentNetwork) { 91 return targetNetwork == null || targetNetwork.equals(currentNetwork); 92 } 93 94 /** 95 * Truncate a service name to up to maxLength UTF-8 bytes. 96 */ truncateServiceName(@onNull String originalName, int maxLength)97 public static String truncateServiceName(@NonNull String originalName, int maxLength) { 98 // UTF-8 is at most 4 bytes per character; return early in the common case where 99 // the name can't possibly be over the limit given its string length. 100 if (originalName.length() <= maxLength / 4) return originalName; 101 102 final Charset utf8 = StandardCharsets.UTF_8; 103 final CharsetEncoder encoder = utf8.newEncoder(); 104 final ByteBuffer out = ByteBuffer.allocate(maxLength); 105 // encode will write as many characters as possible to the out buffer, and just 106 // return an overflow code if there were too many characters (no need to check the 107 // return code here, this method truncates the name on purpose). 108 encoder.encode(CharBuffer.wrap(originalName), out, true /* endOfInput */); 109 return new String(out.array(), 0, out.position(), utf8); 110 } 111 112 /** 113 * Checks if the MdnsRecord needs to be renewed or not. 114 * 115 * <p>As per RFC6762 7.1 no need to query if remaining TTL is more than half the original one, 116 * so send the queries if half the TTL has passed. 117 */ isRecordRenewalNeeded(@onNull MdnsRecord mdnsRecord, final long now)118 public static boolean isRecordRenewalNeeded(@NonNull MdnsRecord mdnsRecord, final long now) { 119 return mdnsRecord.getTtl() > 0 120 && mdnsRecord.getRemainingTTL(now) <= mdnsRecord.getTtl() / 2; 121 } 122 }