[automerger skipped] DO NOT MERGE - Merge Android 10 into master
am: e9f5fb6e81 -s ours
am skip reason: subject contains skip directive

Change-Id: I0bed5bdc2824da431cb62d00bd8f6897e9c4b560
diff --git a/src/com/android/providers/tv/TvProvider.java b/src/com/android/providers/tv/TvProvider.java
index 76bdf8e..78dd724 100644
--- a/src/com/android/providers/tv/TvProvider.java
+++ b/src/com/android/providers/tv/TvProvider.java
@@ -63,6 +63,8 @@
 import com.android.internal.os.SomeArgs;
 import com.android.providers.tv.util.SqlParams;
 
+import com.android.providers.tv.util.SqliteTokenFinder;
+import java.util.Locale;
 import libcore.io.IoUtils;
 
 import java.io.ByteArrayOutputStream;
@@ -124,7 +126,6 @@
     private static final String OP_UPDATE = "update";
     private static final String OP_DELETE = "delete";
 
-
     private static final UriMatcher sUriMatcher;
     private static final int MATCH_CHANNEL = 1;
     private static final int MATCH_CHANNEL_ID = 2;
@@ -1660,13 +1661,33 @@
         }
         Map<String, String> columnProjectionMap = new HashMap<>();
         for (String columnName : projection) {
-            // Value NULL will be provided if the requested column does not exist in the database.
-            columnProjectionMap.put(columnName,
-                     projectionMap.getOrDefault(columnName, "NULL as " + columnName));
+            String value = projectionMap.get(columnName);
+            if (value != null) {
+                columnProjectionMap.put(columnName, value);
+            } else {
+                // Value NULL will be provided if the requested column does not exist in the
+                // database.
+                value = "NULL AS " + DatabaseUtils.sqlEscapeString(columnName);
+                columnProjectionMap.put(columnName, value);
+
+                if (needEventLog(columnName)) {
+                    android.util.EventLog.writeEvent(0x534e4554, "135269669", -1, "");
+                }
+            }
         }
         return columnProjectionMap;
     }
 
+    private boolean needEventLog(String columnName) {
+        for (int i = 0; i < columnName.length(); i++) {
+            char c = columnName.charAt(i);
+            if (!Character.isLetterOrDigit(c) && c != '_') {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void filterContentValues(ContentValues values, Map<String, String> projectionMap) {
         Iterator<String> iter = values.keySet().iterator();
         while (iter.hasNext()) {
@@ -1680,6 +1701,18 @@
     private SqlParams createSqlParams(String operation, Uri uri, String selection,
             String[] selectionArgs) {
         int match = sUriMatcher.match(uri);
+
+        SqliteTokenFinder.findTokens(selection, p -> {
+            if (p.first == SqliteTokenFinder.TYPE_REGULAR
+                    && TextUtils.equals(p.second.toUpperCase(Locale.US), "SELECT")) {
+                // only when a keyword is not in quotes or brackets
+                // see https://www.sqlite.org/lang_keywords.html
+                android.util.EventLog.writeEvent(0x534e4554, "135269669", -1, "");
+                throw new SecurityException(
+                        "Subquery is not allowed in selection: " + selection);
+            }
+        });
+
         SqlParams params = new SqlParams(null, selection, selectionArgs);
 
         // Control access to EPG data (excluding watched programs) when the caller doesn't have all
diff --git a/src/com/android/providers/tv/util/SqliteTokenFinder.java b/src/com/android/providers/tv/util/SqliteTokenFinder.java
new file mode 100644
index 0000000..bb0bac2
--- /dev/null
+++ b/src/com/android/providers/tv/util/SqliteTokenFinder.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2019 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.tv.util;
+
+
+import android.annotation.Nullable;
+
+import android.util.Pair;
+import java.util.function.Consumer;
+
+/**
+ * Simple SQL parser to check statements for usage of prohibited/sensitive fields. Modified from
+ * packages/providers/ContactsProvider/src/com/android/providers/contacts/sqlite/SqlChecker.java
+ */
+public class SqliteTokenFinder {
+    public static final int TYPE_REGULAR = 0;
+    public static final int TYPE_IN_SINGLE_QUOTES = 1;
+    public static final int TYPE_IN_DOUBLE_QUOTES = 2;
+    public static final int TYPE_IN_BACKQUOTES = 3;
+    public static final int TYPE_IN_BRACKETS = 4;
+
+    private static boolean isAlpha(char ch) {
+        return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_');
+    }
+
+    private static boolean isNum(char ch) {
+        return ('0' <= ch && ch <= '9');
+    }
+
+    private static boolean isAlNum(char ch) {
+        return isAlpha(ch) || isNum(ch);
+    }
+
+    private static boolean isAnyOf(char ch, String set) {
+        return set.indexOf(ch) >= 0;
+    }
+
+    private static char peek(String s, int index) {
+        return index < s.length() ? s.charAt(index) : '\0';
+    }
+
+    /**
+     * SQL Tokenizer specialized to extract tokens from SQL (snippets).
+     *
+     * Based on sqlite3GetToken() in tokenzie.c in SQLite.
+     *
+     * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7
+     * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922)
+     *
+     * Also draft spec: http://www.sqlite.org/draft/tokenreq.html
+     *
+     * @param sql the SQL clause to be tokenized.
+     * @param checker the {@link Consumer} to check each token. The input of the checker is a pair
+     *                of token type and the token.
+     */
+    public static void findTokens(@Nullable String sql, Consumer<Pair<Integer, String>> checker) {
+        if (sql == null) {
+            return;
+        }
+        int pos = 0;
+        final int len = sql.length();
+        while (pos < len) {
+            final char ch = peek(sql, pos);
+
+            // Regular token.
+            if (isAlpha(ch)) {
+                final int start = pos;
+                pos++;
+                while (isAlNum(peek(sql, pos))) {
+                    pos++;
+                }
+                final int end = pos;
+
+                final String token = sql.substring(start, end);
+                checker.accept(Pair.create(TYPE_REGULAR, token));
+
+                continue;
+            }
+
+            // Handle quoted tokens
+            if (isAnyOf(ch, "'\"`")) {
+                final int quoteStart = pos;
+                pos++;
+
+                for (;;) {
+                    pos = sql.indexOf(ch, pos);
+                    if (pos < 0) {
+                        throw new IllegalArgumentException("Unterminated quote in" + sql);
+                    }
+                    if (peek(sql, pos + 1) != ch) {
+                        break;
+                    }
+                    // Quoted quote char -- e.g. "abc""def" is a single string.
+                    pos += 2;
+                }
+                final int quoteEnd = pos;
+                pos++;
+
+                // Extract the token
+                String token = sql.substring(quoteStart + 1, quoteEnd);
+                // Unquote if needed. i.e. "aa""bb" -> aa"bb
+                if (token.indexOf(ch) >= 0) {
+                    token = token.replaceAll(String.valueOf(ch) + ch, String.valueOf(ch));
+                }
+                int type = TYPE_REGULAR;
+                switch (ch) {
+                    case '\'':
+                        type = TYPE_IN_SINGLE_QUOTES;
+                        break;
+                    case '\"':
+                        type = TYPE_IN_DOUBLE_QUOTES;
+                        break;
+                    case '`':
+                        type = TYPE_IN_BACKQUOTES;
+                        break;
+                }
+                checker.accept(Pair.create(type, token));
+                continue;
+            }
+            // Handle tokens enclosed in [...]
+            if (ch == '[') {
+                final int quoteStart = pos;
+                pos++;
+
+                pos = sql.indexOf(']', pos);
+                if (pos < 0) {
+                    throw new IllegalArgumentException("Unterminated quote in" + sql);
+                }
+                final int quoteEnd = pos;
+                pos++;
+
+                final String token = sql.substring(quoteStart + 1, quoteEnd);
+
+                checker.accept(Pair.create(TYPE_IN_BRACKETS, token));
+                continue;
+            }
+
+            // Detect comments.
+            if (ch == '-' && peek(sql, pos + 1) == '-') {
+                pos += 2;
+                pos = sql.indexOf('\n', pos);
+                if (pos < 0) {
+                    // strings ending in an inline comment.
+                    break;
+                }
+                pos++;
+
+                continue;
+            }
+            if (ch == '/' && peek(sql, pos + 1) == '*') {
+                pos += 2;
+                pos = sql.indexOf("*/", pos);
+                if (pos < 0) {
+                    throw new IllegalArgumentException("Unterminated comment in" + sql);
+                }
+                pos += 2;
+
+                continue;
+            }
+
+            // For this purpose, we can simply ignore other characters.
+            // (Note it doesn't handle the X'' literal properly and reports this X as a token,
+            // but that should be fine...)
+            pos++;
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/tv/TvProviderForTesting.java b/tests/src/com/android/providers/tv/TvProviderForTesting.java
index 0f9638e..5300756 100644
--- a/tests/src/com/android/providers/tv/TvProviderForTesting.java
+++ b/tests/src/com/android/providers/tv/TvProviderForTesting.java
@@ -21,6 +21,7 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.media.tv.TvContract;
 import android.net.Uri;
+import java.io.File;
 
 class TvProviderForTesting extends TvProvider {
     private static final String FAKE_SESSION_TOKEN = "TvProviderForTesting";
@@ -51,7 +52,10 @@
         super.shutdown();
 
         if (mDatabaseHelper != null) {
+            SQLiteDatabase db = mDatabaseHelper.getWritableDatabase();
+            File databaseFile = new File(db.getPath());
             mDatabaseHelper.close();
+            SQLiteDatabase.deleteDatabase(databaseFile);
         }
     }
 
diff --git a/tests/src/com/android/providers/tv/UnrecognizedColumnsTest.java b/tests/src/com/android/providers/tv/UnrecognizedColumnsTest.java
new file mode 100644
index 0000000..f552013
--- /dev/null
+++ b/tests/src/com/android/providers/tv/UnrecognizedColumnsTest.java
@@ -0,0 +1,116 @@
+package com.android.providers.tv;
+
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.media.tv.TvContract;
+import android.media.tv.TvContract.Programs;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.test.AndroidTestCase;
+import android.test.mock.MockContentProvider;
+import android.test.mock.MockContentResolver;
+import com.android.providers.tv.Utils.Program;
+import java.util.Arrays;
+
+import com.google.android.collect.Sets;
+
+public class UnrecognizedColumnsTest extends AndroidTestCase {
+    private static final String PERMISSION_ACCESS_ALL_EPG_DATA =
+            "com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA";
+    private static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
+
+    private static final String MY_PACKAGE = "example.my";
+    private static final String ANOTHER_PACKAGE = "example.another";
+
+    private MockContentResolver mResolver;
+    private TvProviderForTesting mProvider;
+    private MockTvProviderContext mContext;
+    private Program mProgram;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mResolver = new MockContentResolver();
+        mResolver.addProvider(Settings.AUTHORITY, new MockContentProvider() {
+            @Override
+            public Bundle call(String method, String request, Bundle args) {
+                return new Bundle();
+            }
+        });
+
+        mProvider = new TvProviderForTesting();
+        mResolver.addProvider(TvContract.AUTHORITY, mProvider);
+
+        mContext = new MockTvProviderContext(mResolver, getContext());
+        // get data of the calling package only
+        mContext.grantOrRejectPermission(PERMISSION_ACCESS_ALL_EPG_DATA, false);
+        mContext.grantOrRejectPermission(PERMISSION_READ_TV_LISTINGS, false);
+
+        setContext(mContext);
+
+        final ProviderInfo info = new ProviderInfo();
+        info.authority = TvContract.AUTHORITY;
+        mProvider.attachInfoForTesting(getContext(), info);
+    }
+
+
+    @Override
+    protected void tearDown() throws Exception {
+        Utils.clearTvProvider(mResolver);
+        mProvider.setOpenHelper(null, true);
+        mProvider.shutdown();
+        super.tearDown();
+    }
+
+    public void testUnrecognizedColumns() {
+        insertPrograms();
+
+        String[] projection = new String[] {
+            TvContract.Programs._ID,
+            "_random_name",
+            " with spaces ",
+            "\' in single quotes \'",
+            "\" in double quotes \"",
+            "quotes \' inside \' this \" name \"",
+        };
+
+        Cursor cursor =
+            mResolver.query(TvContract.Programs.CONTENT_URI, projection, null, null, null);
+        assertNotNull(cursor);
+        cursor.moveToNext();
+        assertEquals(1, cursor.getCount());
+
+        assertEquals(
+            "Column names don't match.",
+            Arrays.asList(
+                    Programs._ID,
+                    "_random_name",
+                    " with spaces ",
+                    "\' in single quotes \'",
+                    "\" in double quotes \"",
+                    "quotes \' inside \' this \" name \""),
+            Arrays.asList(cursor.getColumnNames()));
+
+        assertEquals(mProgram.id, cursor.getLong(0));
+        assertNull(cursor.getString(1));
+        assertNull(cursor.getString(2));
+        assertNull(cursor.getString(3));
+        assertNull(cursor.getString(4));
+        assertNull(cursor.getString(5));
+    }
+
+    private void insertPrograms() {
+        mProvider.callingPackage = MY_PACKAGE;
+        long myChannelId = Utils.insertChannel(mResolver);
+        mProgram = new Program(1, MY_PACKAGE);
+        Utils.insertPrograms(mResolver, myChannelId, mProgram);
+
+        mProvider.callingPackage = ANOTHER_PACKAGE;
+        long anotherChannelId = Utils.insertChannel(mResolver);
+        Program anotherProgram = new Program(2, ANOTHER_PACKAGE);
+        Utils.insertPrograms(mResolver, anotherChannelId, anotherProgram);
+
+        mProvider.callingPackage = MY_PACKAGE;
+    }
+}
diff --git a/tests/src/com/android/providers/tv/util/SqliteTokenFinderTest.java b/tests/src/com/android/providers/tv/util/SqliteTokenFinderTest.java
new file mode 100644
index 0000000..f228080
--- /dev/null
+++ b/tests/src/com/android/providers/tv/util/SqliteTokenFinderTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2019 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.tv.util;
+
+import android.annotation.Nullable;
+import android.test.AndroidTestCase;
+import android.util.Pair;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link SqliteTokenFinder}.
+ *
+ * Modified from
+ * packages/providers/ContactsProvider/tests/src/com/android/providers/contacts/sqlite/SqlCheckerTest.java
+ */
+public class SqliteTokenFinderTest extends AndroidTestCase {
+    private List<Pair<Integer, String>> getTokens(String sql) {
+        List<Pair<Integer, String>> tokens = new ArrayList<>();
+        SqliteTokenFinder.findTokens(sql, new Consumer<Pair<Integer, String>>() {
+            @Override
+            public void accept(Pair<Integer, String> pair) {
+                tokens.add(pair);
+            }
+        });
+        return tokens;
+    }
+
+    private void checkTokens(String sql, Pair<Integer, String>... tokens) {
+        final List<Pair<Integer, String>> expected = Arrays.asList(tokens);
+
+        assertEquals(expected, getTokens(sql));
+    }
+
+    private void checkTokensRegular(String sql, @Nullable String tokens) {
+        List<Pair<Integer, String>> expected = new ArrayList<>();
+
+        if (tokens != null) {
+            for (String token : tokens.split(" ")) {
+                expected.add(Pair.create(SqliteTokenFinder.TYPE_REGULAR, token));
+            }
+        }
+
+        assertEquals(expected, getTokens(sql));
+    }
+
+    private void assertInvalidSql(String sql, String message) {
+        try {
+            getTokens(sql);
+            fail("Didn't throw Exception");
+        } catch (Exception e) {
+            assertTrue("Expected " + e.getMessage() + " to contain " + message,
+                    e.getMessage().contains(message));
+        }
+    }
+
+    public void testWhitespaces() {
+        checkTokensRegular("  select  \t\r\n a\n\n  ", "select a");
+        checkTokensRegular("a b", "a b");
+    }
+
+    public void testComment() {
+        checkTokensRegular("--\n", null);
+        checkTokensRegular("a--\n", "a");
+        checkTokensRegular("a--abcdef\n", "a");
+        checkTokensRegular("a--abcdef\nx", "a x");
+        checkTokensRegular("a--\nx", "a x");
+        checkTokensRegular("a--abcdef", "a");
+        checkTokensRegular("a--abcdef\ndef--", "a def");
+
+        checkTokensRegular("/**/", null);
+        assertInvalidSql("/*", "Unterminated comment");
+        assertInvalidSql("/*/", "Unterminated comment");
+        assertInvalidSql("/*\n* /*a", "Unterminated comment");
+        checkTokensRegular("a/**/", "a");
+        checkTokensRegular("/**/b", "b");
+        checkTokensRegular("a/**/b", "a b");
+        checkTokensRegular("a/* -- \n* /* **/b", "a b");
+    }
+
+    public void testSingleQuotes() {
+        assertInvalidSql("'", "Unterminated quote");
+        assertInvalidSql("a'", "Unterminated quote");
+        assertInvalidSql("a'''", "Unterminated quote");
+        assertInvalidSql("a''' ", "Unterminated quote");
+        checkTokens("''", Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, ""));
+
+        // 2 consecutive quotes inside quotes stands for a quote. e.g.'let''s go' -> let's go
+        checkTokens(
+                "''''",
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "\'"));
+        checkTokens(
+                "a''''b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "\'"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a' '' 'b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, " \' "),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("'abc'", Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc"));
+        checkTokens("'abc\ndef'", Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef"));
+        checkTokens(
+                "a'abc\ndef'",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef"));
+        checkTokens(
+                "'abc\ndef'b",
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("a'abc\ndef'b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a'''abc\nd''ef'''b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "\'abc\nd\'ef\'"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+    }
+
+    public void testDoubleQuotes() {
+        assertInvalidSql("\"", "Unterminated quote");
+        assertInvalidSql("a\"", "Unterminated quote");
+        assertInvalidSql("a\"\"\"", "Unterminated quote");
+        assertInvalidSql("a\"\"\" ", "Unterminated quote");
+        checkTokens("\"\"", Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, ""));
+        checkTokens("\"\"\"\"", Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\""));
+        checkTokens(
+                "a\"\"\"\"b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\""),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("a\"\t\"\"\t\"b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\t\"\t"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("\"abc\"", Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc"));
+        checkTokens(
+                "\"abc\ndef\"",
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef"));
+        checkTokens(
+                "a\"abc\ndef\"",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef"));
+        checkTokens(
+                "\"abc\ndef\"b",
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("a\"abc\ndef\"b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("a\"\"\"abc\nd\"\"ef\"\"\"b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "\"abc\nd\"ef\""),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+    }
+
+    public void testBackquotes() {
+        assertInvalidSql("`", "Unterminated quote");
+        assertInvalidSql("a`", "Unterminated quote");
+        assertInvalidSql("a```", "Unterminated quote");
+        assertInvalidSql("a``` ", "Unterminated quote");
+        checkTokens("``", Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, ""));
+        checkTokens("````", Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "`"));
+        checkTokens(
+                "a````b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "`"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a`\t``\t`b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "\t`\t"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("`abc`", Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc"));
+        checkTokens(
+                "`abc\ndef`",
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef"));
+        checkTokens(
+                "a`abc\ndef`",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef"));
+        checkTokens(
+                "`abc\ndef`b",
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a`abc\ndef`b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a```abc\nd``ef```b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "`abc\nd`ef`"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+    }
+
+    public void testBrackets() {
+        assertInvalidSql("[", "Unterminated quote");
+        assertInvalidSql("a[", "Unterminated quote");
+        assertInvalidSql("a[ ", "Unterminated quote");
+        assertInvalidSql("a[[ ", "Unterminated quote");
+        checkTokens("[]", Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, ""));
+        checkTokens("[[]", Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "["));
+        checkTokens(
+                "a[[]b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "["),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a[\t[\t]b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "\t[\t"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens("[abc]", Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc"));
+        checkTokens(
+                "[abc\ndef]",
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef"));
+        checkTokens(
+                "a[abc\ndef]",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef"));
+        checkTokens(
+                "[abc\ndef]b",
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a[abc\ndef]b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "abc\ndef"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+        checkTokens(
+                "a[[abc\nd[ef[]b",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "[abc\nd[ef["),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "b"));
+    }
+
+    public void testTokens() {
+        checkTokensRegular("a,abc,a00b,_1,_123,abcdef", "a abc a00b _1 _123 abcdef");
+        checkTokens(
+                "a--\nabc/**/a00b''_1'''ABC'''`_123`abc[d]\"e\"f",
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "abc"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "a00b"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, ""),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "_1"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_SINGLE_QUOTES, "'ABC'"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BACKQUOTES, "_123"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "abc"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_BRACKETS, "d"),
+                Pair.create(SqliteTokenFinder.TYPE_IN_DOUBLE_QUOTES, "e"),
+                Pair.create(SqliteTokenFinder.TYPE_REGULAR, "f"));
+    }
+}