1 /*
2  * Copyright (C) 2019 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.providers.tv.util;
18 
19 
20 import android.annotation.Nullable;
21 
22 import android.util.Pair;
23 import java.util.function.Consumer;
24 
25 /**
26  * Simple SQL parser to check statements for usage of prohibited/sensitive fields. Modified from
27  * packages/providers/ContactsProvider/src/com/android/providers/contacts/sqlite/SqlChecker.java
28  */
29 public class SqliteTokenFinder {
30     public static final int TYPE_REGULAR = 0;
31     public static final int TYPE_IN_SINGLE_QUOTES = 1;
32     public static final int TYPE_IN_DOUBLE_QUOTES = 2;
33     public static final int TYPE_IN_BACKQUOTES = 3;
34     public static final int TYPE_IN_BRACKETS = 4;
35 
isAlpha(char ch)36     private static boolean isAlpha(char ch) {
37         return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || (ch == '_');
38     }
39 
isNum(char ch)40     private static boolean isNum(char ch) {
41         return ('0' <= ch && ch <= '9');
42     }
43 
isAlNum(char ch)44     private static boolean isAlNum(char ch) {
45         return isAlpha(ch) || isNum(ch);
46     }
47 
isAnyOf(char ch, String set)48     private static boolean isAnyOf(char ch, String set) {
49         return set.indexOf(ch) >= 0;
50     }
51 
peek(String s, int index)52     private static char peek(String s, int index) {
53         return index < s.length() ? s.charAt(index) : '\0';
54     }
55 
56     /**
57      * SQL Tokenizer specialized to extract tokens from SQL (snippets).
58      *
59      * Based on sqlite3GetToken() in tokenzie.c in SQLite.
60      *
61      * Source for v3.8.6 (which android uses): http://www.sqlite.org/src/artifact/ae45399d6252b4d7
62      * (Latest source as of now: http://www.sqlite.org/src/artifact/78c8085bc7af1922)
63      *
64      * Also draft spec: http://www.sqlite.org/draft/tokenreq.html
65      *
66      * @param sql the SQL clause to be tokenized.
67      * @param checker the {@link Consumer} to check each token. The input of the checker is a pair
68      *                of token type and the token.
69      */
findTokens(@ullable String sql, Consumer<Pair<Integer, String>> checker)70     public static void findTokens(@Nullable String sql, Consumer<Pair<Integer, String>> checker) {
71         if (sql == null) {
72             return;
73         }
74         int pos = 0;
75         final int len = sql.length();
76         while (pos < len) {
77             final char ch = peek(sql, pos);
78 
79             // Regular token.
80             if (isAlpha(ch)) {
81                 final int start = pos;
82                 pos++;
83                 while (isAlNum(peek(sql, pos))) {
84                     pos++;
85                 }
86                 final int end = pos;
87 
88                 final String token = sql.substring(start, end);
89                 checker.accept(Pair.create(TYPE_REGULAR, token));
90 
91                 continue;
92             }
93 
94             // Handle quoted tokens
95             if (isAnyOf(ch, "'\"`")) {
96                 final int quoteStart = pos;
97                 pos++;
98 
99                 for (;;) {
100                     pos = sql.indexOf(ch, pos);
101                     if (pos < 0) {
102                         throw new IllegalArgumentException("Unterminated quote in" + sql);
103                     }
104                     if (peek(sql, pos + 1) != ch) {
105                         break;
106                     }
107                     // Quoted quote char -- e.g. "abc""def" is a single string.
108                     pos += 2;
109                 }
110                 final int quoteEnd = pos;
111                 pos++;
112 
113                 // Extract the token
114                 String token = sql.substring(quoteStart + 1, quoteEnd);
115                 // Unquote if needed. i.e. "aa""bb" -> aa"bb
116                 if (token.indexOf(ch) >= 0) {
117                     token = token.replaceAll(String.valueOf(ch) + ch, String.valueOf(ch));
118                 }
119                 int type = TYPE_REGULAR;
120                 switch (ch) {
121                     case '\'':
122                         type = TYPE_IN_SINGLE_QUOTES;
123                         break;
124                     case '\"':
125                         type = TYPE_IN_DOUBLE_QUOTES;
126                         break;
127                     case '`':
128                         type = TYPE_IN_BACKQUOTES;
129                         break;
130                 }
131                 checker.accept(Pair.create(type, token));
132                 continue;
133             }
134             // Handle tokens enclosed in [...]
135             if (ch == '[') {
136                 final int quoteStart = pos;
137                 pos++;
138 
139                 pos = sql.indexOf(']', pos);
140                 if (pos < 0) {
141                     throw new IllegalArgumentException("Unterminated quote in" + sql);
142                 }
143                 final int quoteEnd = pos;
144                 pos++;
145 
146                 final String token = sql.substring(quoteStart + 1, quoteEnd);
147 
148                 checker.accept(Pair.create(TYPE_IN_BRACKETS, token));
149                 continue;
150             }
151 
152             // Detect comments.
153             if (ch == '-' && peek(sql, pos + 1) == '-') {
154                 pos += 2;
155                 pos = sql.indexOf('\n', pos);
156                 if (pos < 0) {
157                     // strings ending in an inline comment.
158                     break;
159                 }
160                 pos++;
161 
162                 continue;
163             }
164             if (ch == '/' && peek(sql, pos + 1) == '*') {
165                 pos += 2;
166                 pos = sql.indexOf("*/", pos);
167                 if (pos < 0) {
168                     throw new IllegalArgumentException("Unterminated comment in" + sql);
169                 }
170                 pos += 2;
171 
172                 continue;
173             }
174 
175             // For this purpose, we can simply ignore other characters.
176             // (Note it doesn't handle the X'' literal properly and reports this X as a token,
177             // but that should be fine...)
178             pos++;
179         }
180     }
181 }
182