| /* |
| * 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.telephony; |
| |
| import android.annotation.Nullable; |
| |
| import java.util.function.Consumer; |
| |
| /** |
| * Simple SQL parser to check statements for usage of prohibited/sensitive fields. Mostly copied |
| * from |
| * packages/providers/ContactsProvider/src/com/android/providers/contacts/sqlite/SqlChecker.java |
| */ |
| public class SqlTokenFinder{ |
| 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 |
| */ |
| public static void findTokens(@Nullable String sql, Consumer<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(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++; |
| |
| if (ch != '\'') { |
| // Extract the token |
| final String tokenUnquoted = sql.substring(quoteStart + 1, quoteEnd); |
| |
| final String token; |
| |
| // Unquote if needed. i.e. "aa""bb" -> aa"bb |
| if (tokenUnquoted.indexOf(ch) >= 0) { |
| token = tokenUnquoted.replaceAll( |
| String.valueOf(ch) + ch, String.valueOf(ch)); |
| } else { |
| token = tokenUnquoted; |
| } |
| checker.accept(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(token); |
| continue; |
| } |
| |
| // Detect comments. |
| if (ch == '-' && peek(sql, pos + 1) == '-') { |
| pos += 2; |
| pos = sql.indexOf('\n', pos); |
| if (pos < 0) { |
| // We disallow strings ending in an inline comment. |
| throw new IllegalArgumentException("Unterminated comment in" + sql); |
| } |
| 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; |
| } |
| |
| // Semicolon is never allowed. |
| if (ch == ';') { |
| throw new IllegalArgumentException("Semicolon is not allowed in " + sql); |
| } |
| |
| // 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++; |
| } |
| } |
| } |