Loading android/app/src/com/android/bluetooth/Utils.java +43 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } } android/app/src/com/android/bluetooth/btservice/AdapterProperties.java +6 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); } } Loading android/app/tests/unit/src/com/android/bluetooth/UtilsTest.java +70 −0 Original line number Diff line number Diff line Loading @@ -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; /** Loading Loading @@ -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); } } android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterPropertiesTest.java +48 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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)); } } Loading
android/app/src/com/android/bluetooth/Utils.java +43 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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; } }
android/app/src/com/android/bluetooth/btservice/AdapterProperties.java +6 −1 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()); } } Loading
android/app/tests/unit/src/com/android/bluetooth/UtilsTest.java +70 −0 Original line number Diff line number Diff line Loading @@ -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; /** Loading Loading @@ -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); } }
android/app/tests/unit/src/com/android/bluetooth/btservice/AdapterPropertiesTest.java +48 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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)); } }