Avoid stringbuilder explosion

Change-Id: Ifd60098d5a5738063a5b5831a857802327f42774
Fixes: 76175892
Test: ContactsProviderTests
diff --git a/src/com/android/providers/contacts/SearchIndexManager.java b/src/com/android/providers/contacts/SearchIndexManager.java
index 14c78a7..e421654 100644
--- a/src/com/android/providers/contacts/SearchIndexManager.java
+++ b/src/com/android/providers/contacts/SearchIndexManager.java
@@ -24,7 +24,6 @@
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.ProviderStatus;
 import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 import android.util.ArraySet;
@@ -35,6 +34,8 @@
 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.util.CappedStringBuilder;
+
 import com.google.android.collect.Lists;
 import com.google.common.annotations.VisibleForTesting;
 
@@ -51,6 +52,8 @@
 
     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
 
+    private static final int MAX_STRING_BUILDER_SIZE = 1024 * 10;
+
     public static final String PROPERTY_SEARCH_INDEX_VERSION = "search_index";
     private static final int SEARCH_INDEX_VERSION = 1;
 
@@ -72,10 +75,11 @@
         public static final int SEPARATOR_SLASH = 2;
         public static final int SEPARATOR_COMMA = 3;
 
-        private StringBuilder mSbContent = new StringBuilder();
-        private StringBuilder mSbName = new StringBuilder();
-        private StringBuilder mSbTokens = new StringBuilder();
-        private StringBuilder mSbElementContent = new StringBuilder();
+        private CappedStringBuilder mSbContent = new CappedStringBuilder(MAX_STRING_BUILDER_SIZE);
+        private CappedStringBuilder mSbName = new CappedStringBuilder(MAX_STRING_BUILDER_SIZE);
+        private CappedStringBuilder mSbTokens = new CappedStringBuilder(MAX_STRING_BUILDER_SIZE);
+        private CappedStringBuilder mSbElementContent = new CappedStringBuilder(
+                MAX_STRING_BUILDER_SIZE);
         private ArraySet<String> mUniqueElements = new ArraySet<>();
         private Cursor mCursor;
 
@@ -84,10 +88,10 @@
         }
 
         void reset() {
-            mSbContent.setLength(0);
-            mSbTokens.setLength(0);
-            mSbName.setLength(0);
-            mSbElementContent.setLength(0);
+            mSbContent.clear();
+            mSbTokens.clear();
+            mSbName.clear();
+            mSbElementContent.clear();
             mUniqueElements.clear();
         }
 
@@ -126,7 +130,7 @@
                     mSbContent.append(content);
                     mUniqueElements.add(content);
                 }
-                mSbElementContent.setLength(0);
+                mSbElementContent.clear();
             }
         }
 
diff --git a/src/com/android/providers/contacts/util/CappedStringBuilder.java b/src/com/android/providers/contacts/util/CappedStringBuilder.java
new file mode 100644
index 0000000..74b70cf
--- /dev/null
+++ b/src/com/android/providers/contacts/util/CappedStringBuilder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts.util;
+
+import android.util.Log;
+
+import com.android.providers.contacts.AbstractContactsProvider;
+
+public class CappedStringBuilder {
+    private final int mCapSize;
+    private boolean mOver;
+    private final StringBuilder mStringBuilder = new StringBuilder();
+
+    public CappedStringBuilder(int capSize) {
+        mCapSize = capSize;
+    }
+
+    public void clear() {
+        mOver = false;
+        mStringBuilder.setLength(0);
+    }
+
+    public int length() {
+        return mStringBuilder.length();
+    }
+
+    @Override
+    public String toString() {
+        return mStringBuilder.toString();
+    }
+
+    public CappedStringBuilder append(char ch) {
+        if (canAppend(mStringBuilder.length() + 1)) {
+            mStringBuilder.append(ch);
+        }
+        return this;
+    }
+
+    public CappedStringBuilder append(String s) {
+        if (canAppend(mStringBuilder.length() + s.length())) {
+            mStringBuilder.append(s);
+        }
+        return this;
+    }
+
+    private boolean canAppend(int length) {
+        if (mOver || length > mCapSize) {
+            if (!mOver && AbstractContactsProvider.VERBOSE_LOGGING) {
+                Log.w(AbstractContactsProvider.TAG, "String too long! new length=" + length);
+            }
+            mOver = true;
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/tests/src/com/android/providers/contacts/util/CappedStringBuilderTest.java b/tests/src/com/android/providers/contacts/util/CappedStringBuilderTest.java
new file mode 100644
index 0000000..d49b263
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/util/CappedStringBuilderTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.providers.contacts.util;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import junit.framework.TestCase;
+
+/**
+ * Run with:
+ atest /android/pi-dev/packages/providers/ContactsProvider/tests/src/com/android/providers/contacts/util/CappedStringBuilderTest.java
+ */
+@SmallTest
+public class CappedStringBuilderTest extends TestCase {
+    public void testCappedChar() {
+        CappedStringBuilder csb = new CappedStringBuilder(8);
+
+        csb.append("abcd");
+        csb.append("efgh");
+
+        csb.append('x');
+        assertEquals("abcdefgh", csb.toString());
+
+        csb.append("y");
+        csb.append("yz");
+
+        assertEquals("abcdefgh", csb.toString());
+    }
+
+    public void testCappedString() {
+        CappedStringBuilder csb = new CappedStringBuilder(8);
+
+        csb.append("abcd");
+        csb.append("efgh");
+
+        csb.append("x");
+        assertEquals("abcdefgh", csb.toString());
+    }
+
+    public void testClear() {
+        CappedStringBuilder csb = new CappedStringBuilder(8);
+
+        csb.append("abcd");
+        csb.append("efgh");
+
+        csb.append("x");
+
+        assertEquals("abcdefgh", csb.toString());
+
+        csb.clear();
+
+        assertEquals("", csb.toString());
+
+        csb.append("abcd");
+        assertEquals("abcd", csb.toString());
+    }
+
+    public void testAlreadyCapped() {
+        CappedStringBuilder csb = new CappedStringBuilder(4);
+
+        csb.append("abc");
+
+        csb.append("xy");
+
+        // Once capped, further append() will all be blocked.
+        csb.append('z');
+        csb.append("z");
+
+        assertEquals("abc", csb.toString());
+    }
+}