1 /* 2 * Copyright (C) 2008 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.downloads; 18 19 import static android.os.Environment.buildExternalStorageAppCacheDirs; 20 import static android.os.Environment.buildExternalStorageAppFilesDirs; 21 import static android.os.Environment.buildExternalStorageAppMediaDirs; 22 import static android.os.Environment.buildExternalStorageAppObbDirs; 23 import static android.provider.Downloads.Impl.FLAG_REQUIRES_CHARGING; 24 import static android.provider.Downloads.Impl.FLAG_REQUIRES_DEVICE_IDLE; 25 26 import static com.android.providers.downloads.Constants.TAG; 27 28 import android.app.job.JobInfo; 29 import android.app.job.JobScheduler; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.database.Cursor; 33 import android.net.Uri; 34 import android.os.Environment; 35 import android.os.FileUtils; 36 import android.os.Handler; 37 import android.os.HandlerThread; 38 import android.os.Process; 39 import android.os.SystemClock; 40 import android.os.UserHandle; 41 import android.os.storage.StorageManager; 42 import android.os.storage.StorageVolume; 43 import android.provider.Downloads; 44 import android.util.Log; 45 import android.webkit.MimeTypeMap; 46 47 import com.google.common.annotations.VisibleForTesting; 48 49 import java.io.File; 50 import java.io.IOException; 51 import java.util.Random; 52 import java.util.Set; 53 import java.util.regex.Matcher; 54 import java.util.regex.Pattern; 55 56 /** 57 * Some helper functions for the download manager 58 */ 59 public class Helpers { 60 public static Random sRandom = new Random(SystemClock.uptimeMillis()); 61 62 /** Regex used to parse content-disposition headers */ 63 private static final Pattern CONTENT_DISPOSITION_PATTERN = 64 Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); 65 66 private static final Object sUniqueLock = new Object(); 67 68 private static HandlerThread sAsyncHandlerThread; 69 private static Handler sAsyncHandler; 70 71 private static SystemFacade sSystemFacade; 72 private static DownloadNotifier sNotifier; 73 Helpers()74 private Helpers() { 75 } 76 getAsyncHandler()77 public synchronized static Handler getAsyncHandler() { 78 if (sAsyncHandlerThread == null) { 79 sAsyncHandlerThread = new HandlerThread("sAsyncHandlerThread", 80 Process.THREAD_PRIORITY_BACKGROUND); 81 sAsyncHandlerThread.start(); 82 sAsyncHandler = new Handler(sAsyncHandlerThread.getLooper()); 83 } 84 return sAsyncHandler; 85 } 86 87 @VisibleForTesting setSystemFacade(SystemFacade systemFacade)88 public synchronized static void setSystemFacade(SystemFacade systemFacade) { 89 sSystemFacade = systemFacade; 90 } 91 getSystemFacade(Context context)92 public synchronized static SystemFacade getSystemFacade(Context context) { 93 if (sSystemFacade == null) { 94 sSystemFacade = new RealSystemFacade(context); 95 } 96 return sSystemFacade; 97 } 98 getDownloadNotifier(Context context)99 public synchronized static DownloadNotifier getDownloadNotifier(Context context) { 100 if (sNotifier == null) { 101 sNotifier = new DownloadNotifier(context); 102 } 103 return sNotifier; 104 } 105 getString(Cursor cursor, String col)106 public static String getString(Cursor cursor, String col) { 107 return cursor.getString(cursor.getColumnIndexOrThrow(col)); 108 } 109 getInt(Cursor cursor, String col)110 public static int getInt(Cursor cursor, String col) { 111 return cursor.getInt(cursor.getColumnIndexOrThrow(col)); 112 } 113 scheduleJob(Context context, long downloadId)114 public static void scheduleJob(Context context, long downloadId) { 115 final boolean scheduled = scheduleJob(context, 116 DownloadInfo.queryDownloadInfo(context, downloadId)); 117 if (!scheduled) { 118 // If we didn't schedule a future job, kick off a notification 119 // update pass immediately 120 getDownloadNotifier(context).update(); 121 } 122 } 123 124 /** 125 * Schedule (or reschedule) a job for the given {@link DownloadInfo} using 126 * its current state to define job constraints. 127 */ scheduleJob(Context context, DownloadInfo info)128 public static boolean scheduleJob(Context context, DownloadInfo info) { 129 if (info == null) return false; 130 131 final JobScheduler scheduler = context.getSystemService(JobScheduler.class); 132 133 // Tear down any existing job for this download 134 final int jobId = (int) info.mId; 135 scheduler.cancel(jobId); 136 137 // Skip scheduling if download is paused or finished 138 if (!info.isReadyToSchedule()) return false; 139 140 final JobInfo.Builder builder = new JobInfo.Builder(jobId, 141 new ComponentName(context, DownloadJobService.class)); 142 143 // When this download will show a notification, run with a higher 144 // priority, since it's effectively a foreground service 145 if (info.isVisible()) { 146 builder.setPriority(JobInfo.PRIORITY_FOREGROUND_APP); 147 builder.setFlags(JobInfo.FLAG_WILL_BE_FOREGROUND); 148 } 149 150 // We might have a backoff constraint due to errors 151 final long latency = info.getMinimumLatency(); 152 if (latency > 0) { 153 builder.setMinimumLatency(latency); 154 } 155 156 // We always require a network, but the type of network might be further 157 // restricted based on download request or user override 158 builder.setRequiredNetworkType(info.getRequiredNetworkType(info.mTotalBytes)); 159 160 if ((info.mFlags & FLAG_REQUIRES_CHARGING) != 0) { 161 builder.setRequiresCharging(true); 162 } 163 if ((info.mFlags & FLAG_REQUIRES_DEVICE_IDLE) != 0) { 164 builder.setRequiresDeviceIdle(true); 165 } 166 167 // If package name was filtered during insert (probably due to being 168 // invalid), blame based on the requesting UID instead 169 String packageName = info.mPackage; 170 if (packageName == null) { 171 packageName = context.getPackageManager().getPackagesForUid(info.mUid)[0]; 172 } 173 174 scheduler.scheduleAsPackage(builder.build(), packageName, UserHandle.myUserId(), TAG); 175 return true; 176 } 177 178 /* 179 * Parse the Content-Disposition HTTP Header. The format of the header 180 * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html 181 * This header provides a filename for content that is going to be 182 * downloaded to the file system. We only support the attachment type. 183 */ parseContentDisposition(String contentDisposition)184 private static String parseContentDisposition(String contentDisposition) { 185 try { 186 Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); 187 if (m.find()) { 188 return m.group(1); 189 } 190 } catch (IllegalStateException ex) { 191 // This function is defined as returning null when it can't parse the header 192 } 193 return null; 194 } 195 196 /** 197 * Creates a filename (where the file should be saved) from info about a download. 198 * This file will be touched to reserve it. 199 */ generateSaveFile(Context context, String url, String hint, String contentDisposition, String contentLocation, String mimeType, int destination)200 static String generateSaveFile(Context context, String url, String hint, 201 String contentDisposition, String contentLocation, String mimeType, int destination) 202 throws IOException { 203 204 final File parent; 205 final File[] parentTest; 206 String name = null; 207 208 if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 209 final File file = new File(Uri.parse(hint).getPath()); 210 parent = file.getParentFile().getAbsoluteFile(); 211 parentTest = new File[] { parent }; 212 name = file.getName(); 213 } else { 214 parent = getRunningDestinationDirectory(context, destination); 215 parentTest = new File[] { 216 parent, 217 getSuccessDestinationDirectory(context, destination) 218 }; 219 name = chooseFilename(url, hint, contentDisposition, contentLocation); 220 } 221 222 // Ensure target directories are ready 223 for (File test : parentTest) { 224 if (!(test.isDirectory() || test.mkdirs())) { 225 throw new IOException("Failed to create parent for " + test); 226 } 227 } 228 229 if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) { 230 name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name); 231 } 232 233 final String prefix; 234 final String suffix; 235 final int dotIndex = name.lastIndexOf('.'); 236 final boolean missingExtension = dotIndex < 0; 237 if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 238 // Destination is explicitly set - do not change the extension 239 if (missingExtension) { 240 prefix = name; 241 suffix = ""; 242 } else { 243 prefix = name.substring(0, dotIndex); 244 suffix = name.substring(dotIndex); 245 } 246 } else { 247 // Split filename between base and extension 248 // Add an extension if filename does not have one 249 if (missingExtension) { 250 prefix = name; 251 suffix = chooseExtensionFromMimeType(mimeType, true); 252 } else { 253 prefix = name.substring(0, dotIndex); 254 suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex); 255 } 256 } 257 258 synchronized (sUniqueLock) { 259 name = generateAvailableFilenameLocked(parentTest, prefix, suffix); 260 261 // Claim this filename inside lock to prevent other threads from 262 // clobbering us. We're not paranoid enough to use O_EXCL. 263 final File file = new File(parent, name); 264 file.createNewFile(); 265 return file.getAbsolutePath(); 266 } 267 } 268 269 private static String chooseFilename(String url, String hint, String contentDisposition, 270 String contentLocation) { 271 String filename = null; 272 273 // First, try to use the hint from the application, if there's one 274 if (filename == null && hint != null && !hint.endsWith("/")) { 275 if (Constants.LOGVV) { 276 Log.v(Constants.TAG, "getting filename from hint"); 277 } 278 int index = hint.lastIndexOf('/') + 1; 279 if (index > 0) { 280 filename = hint.substring(index); 281 } else { 282 filename = hint; 283 } 284 } 285 286 // If we couldn't do anything with the hint, move toward the content disposition 287 if (filename == null && contentDisposition != null) { 288 filename = parseContentDisposition(contentDisposition); 289 if (filename != null) { 290 if (Constants.LOGVV) { 291 Log.v(Constants.TAG, "getting filename from content-disposition"); 292 } 293 int index = filename.lastIndexOf('/') + 1; 294 if (index > 0) { 295 filename = filename.substring(index); 296 } 297 } 298 } 299 300 // If we still have nothing at this point, try the content location 301 if (filename == null && contentLocation != null) { 302 String decodedContentLocation = Uri.decode(contentLocation); 303 if (decodedContentLocation != null 304 && !decodedContentLocation.endsWith("/") 305 && decodedContentLocation.indexOf('?') < 0) { 306 if (Constants.LOGVV) { 307 Log.v(Constants.TAG, "getting filename from content-location"); 308 } 309 int index = decodedContentLocation.lastIndexOf('/') + 1; 310 if (index > 0) { 311 filename = decodedContentLocation.substring(index); 312 } else { 313 filename = decodedContentLocation; 314 } 315 } 316 } 317 318 // If all the other http-related approaches failed, use the plain uri 319 if (filename == null) { 320 String decodedUrl = Uri.decode(url); 321 if (decodedUrl != null 322 && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) { 323 int index = decodedUrl.lastIndexOf('/') + 1; 324 if (index > 0) { 325 if (Constants.LOGVV) { 326 Log.v(Constants.TAG, "getting filename from uri"); 327 } 328 filename = decodedUrl.substring(index); 329 } 330 } 331 } 332 333 // Finally, if couldn't get filename from URI, get a generic filename 334 if (filename == null) { 335 if (Constants.LOGVV) { 336 Log.v(Constants.TAG, "using default filename"); 337 } 338 filename = Constants.DEFAULT_DL_FILENAME; 339 } 340 341 // The VFAT file system is assumed as target for downloads. 342 // Replace invalid characters according to the specifications of VFAT. 343 filename = FileUtils.buildValidFatFilename(filename); 344 345 return filename; 346 } 347 chooseExtensionFromMimeType(String mimeType, boolean useDefaults)348 private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) { 349 String extension = null; 350 if (mimeType != null) { 351 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 352 if (extension != null) { 353 if (Constants.LOGVV) { 354 Log.v(Constants.TAG, "adding extension from type"); 355 } 356 extension = "." + extension; 357 } else { 358 if (Constants.LOGVV) { 359 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 360 } 361 } 362 } 363 if (extension == null) { 364 if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) { 365 if (mimeType.equalsIgnoreCase("text/html")) { 366 if (Constants.LOGVV) { 367 Log.v(Constants.TAG, "adding default html extension"); 368 } 369 extension = Constants.DEFAULT_DL_HTML_EXTENSION; 370 } else if (useDefaults) { 371 if (Constants.LOGVV) { 372 Log.v(Constants.TAG, "adding default text extension"); 373 } 374 extension = Constants.DEFAULT_DL_TEXT_EXTENSION; 375 } 376 } else if (useDefaults) { 377 if (Constants.LOGVV) { 378 Log.v(Constants.TAG, "adding default binary extension"); 379 } 380 extension = Constants.DEFAULT_DL_BINARY_EXTENSION; 381 } 382 } 383 return extension; 384 } 385 chooseExtensionFromFilename(String mimeType, int destination, String filename, int lastDotIndex)386 private static String chooseExtensionFromFilename(String mimeType, int destination, 387 String filename, int lastDotIndex) { 388 String extension = null; 389 if (mimeType != null) { 390 // Compare the last segment of the extension against the mime type. 391 // If there's a mismatch, discard the entire extension. 392 String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 393 filename.substring(lastDotIndex + 1)); 394 if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) { 395 extension = chooseExtensionFromMimeType(mimeType, false); 396 if (extension != null) { 397 if (Constants.LOGVV) { 398 Log.v(Constants.TAG, "substituting extension from type"); 399 } 400 } else { 401 if (Constants.LOGVV) { 402 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 403 } 404 } 405 } 406 } 407 if (extension == null) { 408 if (Constants.LOGVV) { 409 Log.v(Constants.TAG, "keeping extension"); 410 } 411 extension = filename.substring(lastDotIndex); 412 } 413 return extension; 414 } 415 isFilenameAvailableLocked(File[] parents, String name)416 private static boolean isFilenameAvailableLocked(File[] parents, String name) { 417 if (Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(name)) return false; 418 419 for (File parent : parents) { 420 if (new File(parent, name).exists()) { 421 return false; 422 } 423 } 424 425 return true; 426 } 427 generateAvailableFilenameLocked( File[] parents, String prefix, String suffix)428 private static String generateAvailableFilenameLocked( 429 File[] parents, String prefix, String suffix) throws IOException { 430 String name = prefix + suffix; 431 if (isFilenameAvailableLocked(parents, name)) { 432 return name; 433 } 434 435 /* 436 * This number is used to generate partially randomized filenames to avoid 437 * collisions. 438 * It starts at 1. 439 * The next 9 iterations increment it by 1 at a time (up to 10). 440 * The next 9 iterations increment it by 1 to 10 (random) at a time. 441 * The next 9 iterations increment it by 1 to 100 (random) at a time. 442 * ... Up to the point where it increases by 100000000 at a time. 443 * (the maximum value that can be reached is 1000000000) 444 * As soon as a number is reached that generates a filename that doesn't exist, 445 * that filename is used. 446 * If the filename coming in is [base].[ext], the generated filenames are 447 * [base]-[sequence].[ext]. 448 */ 449 int sequence = 1; 450 for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { 451 for (int iteration = 0; iteration < 9; ++iteration) { 452 name = prefix + Constants.FILENAME_SEQUENCE_SEPARATOR + sequence + suffix; 453 if (isFilenameAvailableLocked(parents, name)) { 454 return name; 455 } 456 sequence += sRandom.nextInt(magnitude) + 1; 457 } 458 } 459 460 throw new IOException("Failed to generate an available filename"); 461 } 462 isFilenameValid(Context context, File file)463 static boolean isFilenameValid(Context context, File file) { 464 return isFilenameValid(context, file, true); 465 } 466 isFilenameValidInExternal(Context context, File file)467 static boolean isFilenameValidInExternal(Context context, File file) { 468 return isFilenameValid(context, file, false); 469 } 470 471 /** 472 * Test if given file exists in one of the package-specific external storage 473 * directories that are always writable to apps, regardless of storage 474 * permission. 475 */ isFilenameValidInExternalPackage(Context context, File file, String packageName)476 static boolean isFilenameValidInExternalPackage(Context context, File file, 477 String packageName) { 478 try { 479 if (containsCanonical(buildExternalStorageAppFilesDirs(packageName), file) || 480 containsCanonical(buildExternalStorageAppObbDirs(packageName), file) || 481 containsCanonical(buildExternalStorageAppCacheDirs(packageName), file) || 482 containsCanonical(buildExternalStorageAppMediaDirs(packageName), file)) { 483 return true; 484 } 485 } catch (IOException e) { 486 Log.w(TAG, "Failed to resolve canonical path: " + e); 487 return false; 488 } 489 490 Log.w(TAG, "Path appears to be invalid: " + file); 491 return false; 492 } 493 494 /** 495 * Checks whether the filename looks legitimate for security purposes. This 496 * prevents us from opening files that aren't actually downloads. 497 */ isFilenameValid(Context context, File file, boolean allowInternal)498 static boolean isFilenameValid(Context context, File file, boolean allowInternal) { 499 try { 500 if (allowInternal) { 501 if (containsCanonical(context.getFilesDir(), file) 502 || containsCanonical(context.getCacheDir(), file) 503 || containsCanonical(Environment.getDownloadCacheDirectory(), file)) { 504 return true; 505 } 506 } 507 508 final StorageVolume[] volumes = StorageManager.getVolumeList(UserHandle.myUserId(), 509 StorageManager.FLAG_FOR_WRITE); 510 for (StorageVolume volume : volumes) { 511 if (containsCanonical(volume.getPathFile(), file)) { 512 return true; 513 } 514 } 515 } catch (IOException e) { 516 Log.w(TAG, "Failed to resolve canonical path: " + e); 517 return false; 518 } 519 520 Log.w(TAG, "Path appears to be invalid: " + file); 521 return false; 522 } 523 containsCanonical(File dir, File file)524 private static boolean containsCanonical(File dir, File file) throws IOException { 525 return FileUtils.contains(dir.getCanonicalFile(), file); 526 } 527 containsCanonical(File[] dirs, File file)528 private static boolean containsCanonical(File[] dirs, File file) throws IOException { 529 for (File dir : dirs) { 530 if (containsCanonical(dir, file)) { 531 return true; 532 } 533 } 534 return false; 535 } 536 getRunningDestinationDirectory(Context context, int destination)537 public static File getRunningDestinationDirectory(Context context, int destination) 538 throws IOException { 539 return getDestinationDirectory(context, destination, true); 540 } 541 getSuccessDestinationDirectory(Context context, int destination)542 public static File getSuccessDestinationDirectory(Context context, int destination) 543 throws IOException { 544 return getDestinationDirectory(context, destination, false); 545 } 546 getDestinationDirectory(Context context, int destination, boolean running)547 private static File getDestinationDirectory(Context context, int destination, boolean running) 548 throws IOException { 549 switch (destination) { 550 case Downloads.Impl.DESTINATION_CACHE_PARTITION: 551 case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: 552 case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: 553 if (running) { 554 return context.getFilesDir(); 555 } else { 556 return context.getCacheDir(); 557 } 558 559 case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION: 560 if (running) { 561 return new File(Environment.getDownloadCacheDirectory(), 562 Constants.DIRECTORY_CACHE_RUNNING); 563 } else { 564 return Environment.getDownloadCacheDirectory(); 565 } 566 567 case Downloads.Impl.DESTINATION_EXTERNAL: 568 final File target = new File( 569 Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); 570 if (!target.isDirectory() && target.mkdirs()) { 571 throw new IOException("unable to create external downloads directory"); 572 } 573 return target; 574 575 default: 576 throw new IllegalStateException("unexpected destination: " + destination); 577 } 578 } 579 580 /** 581 * Checks whether this looks like a legitimate selection parameter 582 */ validateSelection(String selection, Set<String> allowedColumns)583 public static void validateSelection(String selection, Set<String> allowedColumns) { 584 try { 585 if (selection == null || selection.isEmpty()) { 586 return; 587 } 588 Lexer lexer = new Lexer(selection, allowedColumns); 589 parseExpression(lexer); 590 if (lexer.currentToken() != Lexer.TOKEN_END) { 591 throw new IllegalArgumentException("syntax error"); 592 } 593 } catch (RuntimeException ex) { 594 if (Constants.LOGV) { 595 Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex); 596 } else if (false) { 597 Log.d(Constants.TAG, "invalid selection triggered " + ex); 598 } 599 throw ex; 600 } 601 602 } 603 604 // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] * 605 // | statement [AND_OR expression]* parseExpression(Lexer lexer)606 private static void parseExpression(Lexer lexer) { 607 for (;;) { 608 // ( expression ) 609 if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) { 610 lexer.advance(); 611 parseExpression(lexer); 612 if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) { 613 throw new IllegalArgumentException("syntax error, unmatched parenthese"); 614 } 615 lexer.advance(); 616 } else { 617 // statement 618 parseStatement(lexer); 619 } 620 if (lexer.currentToken() != Lexer.TOKEN_AND_OR) { 621 break; 622 } 623 lexer.advance(); 624 } 625 } 626 627 // statement <- COLUMN COMPARE VALUE 628 // | COLUMN IS NULL parseStatement(Lexer lexer)629 private static void parseStatement(Lexer lexer) { 630 // both possibilities start with COLUMN 631 if (lexer.currentToken() != Lexer.TOKEN_COLUMN) { 632 throw new IllegalArgumentException("syntax error, expected column name"); 633 } 634 lexer.advance(); 635 636 // statement <- COLUMN COMPARE VALUE 637 if (lexer.currentToken() == Lexer.TOKEN_COMPARE) { 638 lexer.advance(); 639 if (lexer.currentToken() != Lexer.TOKEN_VALUE) { 640 throw new IllegalArgumentException("syntax error, expected quoted string"); 641 } 642 lexer.advance(); 643 return; 644 } 645 646 // statement <- COLUMN IS NULL 647 if (lexer.currentToken() == Lexer.TOKEN_IS) { 648 lexer.advance(); 649 if (lexer.currentToken() != Lexer.TOKEN_NULL) { 650 throw new IllegalArgumentException("syntax error, expected NULL"); 651 } 652 lexer.advance(); 653 return; 654 } 655 656 // didn't get anything good after COLUMN 657 throw new IllegalArgumentException("syntax error after column name"); 658 } 659 660 /** 661 * A simple lexer that recognizes the words of our restricted subset of SQL where clauses 662 */ 663 private static class Lexer { 664 public static final int TOKEN_START = 0; 665 public static final int TOKEN_OPEN_PAREN = 1; 666 public static final int TOKEN_CLOSE_PAREN = 2; 667 public static final int TOKEN_AND_OR = 3; 668 public static final int TOKEN_COLUMN = 4; 669 public static final int TOKEN_COMPARE = 5; 670 public static final int TOKEN_VALUE = 6; 671 public static final int TOKEN_IS = 7; 672 public static final int TOKEN_NULL = 8; 673 public static final int TOKEN_END = 9; 674 675 private final String mSelection; 676 private final Set<String> mAllowedColumns; 677 private int mOffset = 0; 678 private int mCurrentToken = TOKEN_START; 679 private final char[] mChars; 680 Lexer(String selection, Set<String> allowedColumns)681 public Lexer(String selection, Set<String> allowedColumns) { 682 mSelection = selection; 683 mAllowedColumns = allowedColumns; 684 mChars = new char[mSelection.length()]; 685 mSelection.getChars(0, mChars.length, mChars, 0); 686 advance(); 687 } 688 currentToken()689 public int currentToken() { 690 return mCurrentToken; 691 } 692 advance()693 public void advance() { 694 char[] chars = mChars; 695 696 // consume whitespace 697 while (mOffset < chars.length && chars[mOffset] == ' ') { 698 ++mOffset; 699 } 700 701 // end of input 702 if (mOffset == chars.length) { 703 mCurrentToken = TOKEN_END; 704 return; 705 } 706 707 // "(" 708 if (chars[mOffset] == '(') { 709 ++mOffset; 710 mCurrentToken = TOKEN_OPEN_PAREN; 711 return; 712 } 713 714 // ")" 715 if (chars[mOffset] == ')') { 716 ++mOffset; 717 mCurrentToken = TOKEN_CLOSE_PAREN; 718 return; 719 } 720 721 // "?" 722 if (chars[mOffset] == '?') { 723 ++mOffset; 724 mCurrentToken = TOKEN_VALUE; 725 return; 726 } 727 728 // "=" and "==" 729 if (chars[mOffset] == '=') { 730 ++mOffset; 731 mCurrentToken = TOKEN_COMPARE; 732 if (mOffset < chars.length && chars[mOffset] == '=') { 733 ++mOffset; 734 } 735 return; 736 } 737 738 // ">" and ">=" 739 if (chars[mOffset] == '>') { 740 ++mOffset; 741 mCurrentToken = TOKEN_COMPARE; 742 if (mOffset < chars.length && chars[mOffset] == '=') { 743 ++mOffset; 744 } 745 return; 746 } 747 748 // "<", "<=" and "<>" 749 if (chars[mOffset] == '<') { 750 ++mOffset; 751 mCurrentToken = TOKEN_COMPARE; 752 if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) { 753 ++mOffset; 754 } 755 return; 756 } 757 758 // "!=" 759 if (chars[mOffset] == '!') { 760 ++mOffset; 761 mCurrentToken = TOKEN_COMPARE; 762 if (mOffset < chars.length && chars[mOffset] == '=') { 763 ++mOffset; 764 return; 765 } 766 throw new IllegalArgumentException("Unexpected character after !"); 767 } 768 769 // columns and keywords 770 // first look for anything that looks like an identifier or a keyword 771 // and then recognize the individual words. 772 // no attempt is made at discarding sequences of underscores with no alphanumeric 773 // characters, even though it's not clear that they'd be legal column names. 774 if (isIdentifierStart(chars[mOffset])) { 775 int startOffset = mOffset; 776 ++mOffset; 777 while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) { 778 ++mOffset; 779 } 780 String word = mSelection.substring(startOffset, mOffset); 781 if (mOffset - startOffset <= 4) { 782 if (word.equals("IS")) { 783 mCurrentToken = TOKEN_IS; 784 return; 785 } 786 if (word.equals("OR") || word.equals("AND")) { 787 mCurrentToken = TOKEN_AND_OR; 788 return; 789 } 790 if (word.equals("NULL")) { 791 mCurrentToken = TOKEN_NULL; 792 return; 793 } 794 } 795 if (mAllowedColumns.contains(word)) { 796 mCurrentToken = TOKEN_COLUMN; 797 return; 798 } 799 throw new IllegalArgumentException("unrecognized column or keyword: " + word); 800 } 801 802 // quoted strings 803 if (chars[mOffset] == '\'') { 804 ++mOffset; 805 while (mOffset < chars.length) { 806 if (chars[mOffset] == '\'') { 807 if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') { 808 ++mOffset; 809 } else { 810 break; 811 } 812 } 813 ++mOffset; 814 } 815 if (mOffset == chars.length) { 816 throw new IllegalArgumentException("unterminated string"); 817 } 818 ++mOffset; 819 mCurrentToken = TOKEN_VALUE; 820 return; 821 } 822 823 // anything we don't recognize 824 throw new IllegalArgumentException("illegal character: " + chars[mOffset]); 825 } 826 isIdentifierStart(char c)827 private static final boolean isIdentifierStart(char c) { 828 return c == '_' || 829 (c >= 'A' && c <= 'Z') || 830 (c >= 'a' && c <= 'z'); 831 } 832 isIdentifierChar(char c)833 private static final boolean isIdentifierChar(char c) { 834 return c == '_' || 835 (c >= 'A' && c <= 'Z') || 836 (c >= 'a' && c <= 'z') || 837 (c >= '0' && c <= '9'); 838 } 839 } 840 } 841