From e1c6797bb0c2d37fa48592fb2a364c6503c2c5c0 Mon Sep 17 00:00:00 2001 From: William Escande <wescande@google.com> Date: Mon, 21 Aug 2023 18:04:25 -0700 Subject: [PATCH] GAP: Allow BT name upto 248 bytes from setName As per BT spec BT name should be of max 248 bytes but setname api allows more than 248 bytes in bt name and bt name in stack and app out of sync. Hence allow only 248 bytes from setName. Bug: 296517746 Test: atest BluetoothInstrumentationTests:AdapterPropertiesTest Test: atest BluetoothInstrumentationTests:UtilsTest Change-Id: Id6c3f1bbac2f71ac7ef0544305c92905e71ddc1e --- .../app/src/com/android/bluetooth/Utils.java | 44 +++++++++++- .../btservice/AdapterProperties.java | 7 +- .../src/com/android/bluetooth/UtilsTest.java | 70 +++++++++++++++++++ .../btservice/AdapterPropertiesTest.java | 48 +++++++++++++ 4 files changed, 167 insertions(+), 2 deletions(-) diff --git a/android/app/src/com/android/bluetooth/Utils.java b/android/app/src/com/android/bluetooth/Utils.java index 9372cb45cf3..a3d149db940 100644 --- a/android/app/src/com/android/bluetooth/Utils.java +++ b/android/app/src/com/android/bluetooth/Utils.java @@ -63,7 +63,6 @@ import android.provider.DeviceConfig; import android.provider.Telephony; import android.util.Log; -import androidx.annotation.RequiresApi; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.ProfileService; @@ -1235,4 +1234,47 @@ public final class Utils { return pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION) || pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK); } + + /** + * Returns the longest prefix of a string for which the UTF-8 encoding fits into the given + * number of bytes, with the additional guarantee that the string is not truncated in the middle + * of a valid surrogate pair. + * + * <p>Unpaired surrogates are counted as taking 3 bytes of storage. However, a subsequent + * attempt to actually encode a string containing unpaired surrogates is likely to be rejected + * by the UTF-8 implementation. + * + * <p>(copied from framework/base/core/java/android/text/TextUtils.java) + * + * @param str a string + * @param maxbytes the maximum number of UTF-8 encoded bytes + * @return the beginning of the string, so that it uses at most maxbytes bytes in UTF-8 + * @throws IndexOutOfBoundsException if maxbytes is negative + */ + public static String truncateStringForUtf8Storage(String str, int maxbytes) { + if (maxbytes < 0) { + throw new IndexOutOfBoundsException(); + } + + int bytes = 0; + for (int i = 0, len = str.length(); i < len; i++) { + char c = str.charAt(i); + if (c < 0x80) { + bytes += 1; + } else if (c < 0x800) { + bytes += 2; + } else if (c < Character.MIN_SURROGATE + || c > Character.MAX_SURROGATE + || str.codePointAt(i) < Character.MIN_SUPPLEMENTARY_CODE_POINT) { + bytes += 3; + } else { + bytes += 4; + i += (bytes > maxbytes) ? 0 : 1; + } + if (bytes > maxbytes) { + return str.substring(0, i); + } + } + return str; + } } diff --git a/android/app/src/com/android/bluetooth/btservice/AdapterProperties.java b/android/app/src/com/android/bluetooth/btservice/AdapterProperties.java index 0dc5d856a20..573057f9fa1 100644 --- a/android/app/src/com/android/bluetooth/btservice/AdapterProperties.java +++ b/android/app/src/com/android/bluetooth/btservice/AdapterProperties.java @@ -82,6 +82,7 @@ class AdapterProperties { "persist.bluetooth.a2dp_offload.disabled"; private static final long DEFAULT_DISCOVERY_TIMEOUT_MS = 12800; + @VisibleForTesting static final int BLUETOOTH_NAME_MAX_LENGTH_BYTES = 248; private static final int BD_ADDR_LEN = 6; // in bytes private volatile String mName; @@ -318,7 +319,11 @@ class AdapterProperties { boolean setName(String name) { synchronized (mObject) { return mService.getNative() - .setAdapterProperty(AbstractionLayer.BT_PROPERTY_BDNAME, name.getBytes()); + .setAdapterProperty( + AbstractionLayer.BT_PROPERTY_BDNAME, + Utils.truncateStringForUtf8Storage( + name, BLUETOOTH_NAME_MAX_LENGTH_BYTES) + .getBytes()); } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/UtilsTest.java b/android/app/tests/unit/src/com/android/bluetooth/UtilsTest.java index 20e830c7baa..ae7752c8881 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/UtilsTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/UtilsTest.java @@ -47,6 +47,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.UUID; /** @@ -249,4 +250,73 @@ public class UtilsTest { doThrow(new IOException()).when(os).close(); Utils.safeCloseStream(os); } + + @Test + public void truncateUtf8_toZeroLength_isEmpty() { + assertThat(Utils.truncateStringForUtf8Storage("abc", 0)).isEmpty(); + } + + @Test + public void truncateUtf8_longCase_isExpectedResult() { + StringBuilder builder = new StringBuilder(); + + int n = 50; + for (int i = 0; i < 2 * n; i++) { + builder.append("哈"); + } + String initial = builder.toString(); + String result = Utils.truncateStringForUtf8Storage(initial, n); + + // Result should be the beginning of initial + assertThat(initial.startsWith(result)).isTrue(); + + // Result should take less than n bytes in UTF-8 + assertThat(result.getBytes(StandardCharsets.UTF_8).length).isAtMost(n); + + // result + the next codePoint should take strictly more than + // n bytes in UTF-8 + assertThat( + initial.substring(0, initial.offsetByCodePoints(result.length(), 1)) + .getBytes(StandardCharsets.UTF_8) + .length) + .isGreaterThan(n); + } + + @Test + public void truncateUtf8_untruncatedString_isEqual() { + String s = "sf\u20ACgk\u00E9ls\u00E9fg"; + assertThat(Utils.truncateStringForUtf8Storage(s, 100)).isEqualTo(s); + } + + @Test + public void truncateUtf8_inMiddleOfSurrogate_isStillUtf8() { + StringBuilder builder = new StringBuilder(); + String beginning = "a"; + builder.append(beginning); + builder.append(Character.toChars(0x1D11E)); + + // \u1D11E is a surrogate and needs 4 bytes in UTF-8. beginning == "a" uses + // only 1 bytes in UTF8 + // As we allow only 3 bytes for the whole string, so just 2 for this + // codePoint, there is not enough place and the string will be truncated + // just before it + assertThat(Utils.truncateStringForUtf8Storage(builder.toString(), 3)).isEqualTo(beginning); + } + + @Test + public void truncateUtf8_inMiddleOfChar_isStillUtf8() { + StringBuilder builder = new StringBuilder(); + String beginning = "a"; + builder.append(beginning); + builder.append(Character.toChars(0x20AC)); + + // Like above, \u20AC uses 3 bytes in UTF-8, with "beginning", that makes + // 4 bytes so it is too big and should be truncated + assertThat(Utils.truncateStringForUtf8Storage(builder.toString(), 3)).isEqualTo(beginning); + } + + @Test(expected = IndexOutOfBoundsException.class) + public void truncateUtf8_toNegativeSize_ThrowsException() { + Utils.truncateStringForUtf8Storage("abc", -1); + } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterPropertiesTest.java b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterPropertiesTest.java index 3ad06842234..436151b46d4 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterPropertiesTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterPropertiesTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,6 +38,7 @@ import com.android.bluetooth.Utils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -110,4 +112,50 @@ public class AdapterPropertiesTest { assertThat(mAdapterProperties.getBondedDevices()[0].getAddress()) .isEqualTo(Utils.getAddressStringFromByte(TEST_BT_ADDR_BYTES_2)); } + + @Test + public void setName_shortName_isEqual() { + StringBuilder builder = new StringBuilder(); + String stringName = "Wonderful Bluetooth Name Using utf8"; + builder.append(stringName); + builder.append(Character.toChars(0x20AC)); + + String initial = builder.toString(); + + final ArgumentCaptor<byte[]> argumentName = ArgumentCaptor.forClass(byte[].class); + + mAdapterProperties.setName(initial); + verify(mNativeInterface) + .setAdapterProperty( + eq(AbstractionLayer.BT_PROPERTY_BDNAME), argumentName.capture()); + + assertThat(argumentName.getValue()).isEqualTo(initial.getBytes()); + } + + @Test + public void setName_tooLongName_isTruncated() { + StringBuilder builder = new StringBuilder(); + String stringName = "Wonderful Bluetooth Name Using utf8 ... But this name is too long"; + builder.append(stringName); + + int n = 300; + for (int i = 0; i < 2 * n; i++) { + builder.append(Character.toChars(0x20AC)); + } + + String initial = builder.toString(); + + final ArgumentCaptor<byte[]> argumentName = ArgumentCaptor.forClass(byte[].class); + + mAdapterProperties.setName(initial); + verify(mNativeInterface) + .setAdapterProperty( + eq(AbstractionLayer.BT_PROPERTY_BDNAME), argumentName.capture()); + + byte[] name = argumentName.getValue(); + + assertThat(name.length).isLessThan(initial.getBytes().length); + + assertThat(initial).startsWith(new String(name)); + } } -- GitLab