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 17package com.android.providers.downloads; 18 19import static com.android.providers.downloads.Constants.TAG; 20 21import android.content.Context; 22import android.net.Uri; 23import android.os.Environment; 24import android.os.FileUtils; 25import android.os.SystemClock; 26import android.provider.Downloads; 27import android.util.Log; 28import android.webkit.MimeTypeMap; 29 30import java.io.File; 31import java.io.IOException; 32import java.util.Random; 33import java.util.Set; 34import java.util.regex.Matcher; 35import java.util.regex.Pattern; 36 37/** 38 * Some helper functions for the download manager 39 */ 40public class Helpers { 41 public static Random sRandom = new Random(SystemClock.uptimeMillis()); 42 43 /** Regex used to parse content-disposition headers */ 44 private static final Pattern CONTENT_DISPOSITION_PATTERN = 45 Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); 46 47 private static final Object sUniqueLock = new Object(); 48 49 private Helpers() { 50 } 51 52 /* 53 * Parse the Content-Disposition HTTP Header. The format of the header 54 * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html 55 * This header provides a filename for content that is going to be 56 * downloaded to the file system. We only support the attachment type. 57 */ 58 private static String parseContentDisposition(String contentDisposition) { 59 try { 60 Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); 61 if (m.find()) { 62 return m.group(1); 63 } 64 } catch (IllegalStateException ex) { 65 // This function is defined as returning null when it can't parse the header 66 } 67 return null; 68 } 69 70 /** 71 * Creates a filename (where the file should be saved) from info about a download. 72 * This file will be touched to reserve it. 73 */ 74 static String generateSaveFile(Context context, String url, String hint, 75 String contentDisposition, String contentLocation, String mimeType, int destination) 76 throws IOException { 77 78 final File parent; 79 final File[] parentTest; 80 String name = null; 81 82 if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 83 final File file = new File(Uri.parse(hint).getPath()); 84 parent = file.getParentFile().getAbsoluteFile(); 85 parentTest = new File[] { parent }; 86 name = file.getName(); 87 } else { 88 parent = getRunningDestinationDirectory(context, destination); 89 parentTest = new File[] { 90 parent, 91 getSuccessDestinationDirectory(context, destination) 92 }; 93 name = chooseFilename(url, hint, contentDisposition, contentLocation); 94 } 95 96 // Ensure target directories are ready 97 for (File test : parentTest) { 98 if (!(test.isDirectory() || test.mkdirs())) { 99 throw new IOException("Failed to create parent for " + test); 100 } 101 } 102 103 if (DownloadDrmHelper.isDrmConvertNeeded(mimeType)) { 104 name = DownloadDrmHelper.modifyDrmFwLockFileExtension(name); 105 } 106 107 final String prefix; 108 final String suffix; 109 final int dotIndex = name.lastIndexOf('.'); 110 final boolean missingExtension = dotIndex < 0; 111 if (destination == Downloads.Impl.DESTINATION_FILE_URI) { 112 // Destination is explicitly set - do not change the extension 113 if (missingExtension) { 114 prefix = name; 115 suffix = ""; 116 } else { 117 prefix = name.substring(0, dotIndex); 118 suffix = name.substring(dotIndex); 119 } 120 } else { 121 // Split filename between base and extension 122 // Add an extension if filename does not have one 123 if (missingExtension) { 124 prefix = name; 125 suffix = chooseExtensionFromMimeType(mimeType, true); 126 } else { 127 prefix = name.substring(0, dotIndex); 128 suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex); 129 } 130 } 131 132 synchronized (sUniqueLock) { 133 name = generateAvailableFilenameLocked(parentTest, prefix, suffix); 134 135 // Claim this filename inside lock to prevent other threads from 136 // clobbering us. We're not paranoid enough to use O_EXCL. 137 final File file = new File(parent, name); 138 file.createNewFile(); 139 return file.getAbsolutePath(); 140 } 141 } 142 143 private static String chooseFilename(String url, String hint, String contentDisposition, 144 String contentLocation) { 145 String filename = null; 146 147 // First, try to use the hint from the application, if there's one 148 if (filename == null && hint != null && !hint.endsWith("/")) { 149 if (Constants.LOGVV) { 150 Log.v(Constants.TAG, "getting filename from hint"); 151 } 152 int index = hint.lastIndexOf('/') + 1; 153 if (index > 0) { 154 filename = hint.substring(index); 155 } else { 156 filename = hint; 157 } 158 } 159 160 // If we couldn't do anything with the hint, move toward the content disposition 161 if (filename == null && contentDisposition != null) { 162 filename = parseContentDisposition(contentDisposition); 163 if (filename != null) { 164 if (Constants.LOGVV) { 165 Log.v(Constants.TAG, "getting filename from content-disposition"); 166 } 167 int index = filename.lastIndexOf('/') + 1; 168 if (index > 0) { 169 filename = filename.substring(index); 170 } 171 } 172 } 173 174 // If we still have nothing at this point, try the content location 175 if (filename == null && contentLocation != null) { 176 String decodedContentLocation = Uri.decode(contentLocation); 177 if (decodedContentLocation != null 178 && !decodedContentLocation.endsWith("/") 179 && decodedContentLocation.indexOf('?') < 0) { 180 if (Constants.LOGVV) { 181 Log.v(Constants.TAG, "getting filename from content-location"); 182 } 183 int index = decodedContentLocation.lastIndexOf('/') + 1; 184 if (index > 0) { 185 filename = decodedContentLocation.substring(index); 186 } else { 187 filename = decodedContentLocation; 188 } 189 } 190 } 191 192 // If all the other http-related approaches failed, use the plain uri 193 if (filename == null) { 194 String decodedUrl = Uri.decode(url); 195 if (decodedUrl != null 196 && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) { 197 int index = decodedUrl.lastIndexOf('/') + 1; 198 if (index > 0) { 199 if (Constants.LOGVV) { 200 Log.v(Constants.TAG, "getting filename from uri"); 201 } 202 filename = decodedUrl.substring(index); 203 } 204 } 205 } 206 207 // Finally, if couldn't get filename from URI, get a generic filename 208 if (filename == null) { 209 if (Constants.LOGVV) { 210 Log.v(Constants.TAG, "using default filename"); 211 } 212 filename = Constants.DEFAULT_DL_FILENAME; 213 } 214 215 // The VFAT file system is assumed as target for downloads. 216 // Replace invalid characters according to the specifications of VFAT. 217 filename = replaceInvalidVfatCharacters(filename); 218 219 return filename; 220 } 221 222 private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) { 223 String extension = null; 224 if (mimeType != null) { 225 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 226 if (extension != null) { 227 if (Constants.LOGVV) { 228 Log.v(Constants.TAG, "adding extension from type"); 229 } 230 extension = "." + extension; 231 } else { 232 if (Constants.LOGVV) { 233 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 234 } 235 } 236 } 237 if (extension == null) { 238 if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) { 239 if (mimeType.equalsIgnoreCase("text/html")) { 240 if (Constants.LOGVV) { 241 Log.v(Constants.TAG, "adding default html extension"); 242 } 243 extension = Constants.DEFAULT_DL_HTML_EXTENSION; 244 } else if (useDefaults) { 245 if (Constants.LOGVV) { 246 Log.v(Constants.TAG, "adding default text extension"); 247 } 248 extension = Constants.DEFAULT_DL_TEXT_EXTENSION; 249 } 250 } else if (useDefaults) { 251 if (Constants.LOGVV) { 252 Log.v(Constants.TAG, "adding default binary extension"); 253 } 254 extension = Constants.DEFAULT_DL_BINARY_EXTENSION; 255 } 256 } 257 return extension; 258 } 259 260 private static String chooseExtensionFromFilename(String mimeType, int destination, 261 String filename, int lastDotIndex) { 262 String extension = null; 263 if (mimeType != null) { 264 // Compare the last segment of the extension against the mime type. 265 // If there's a mismatch, discard the entire extension. 266 String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 267 filename.substring(lastDotIndex + 1)); 268 if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) { 269 extension = chooseExtensionFromMimeType(mimeType, false); 270 if (extension != null) { 271 if (Constants.LOGVV) { 272 Log.v(Constants.TAG, "substituting extension from type"); 273 } 274 } else { 275 if (Constants.LOGVV) { 276 Log.v(Constants.TAG, "couldn't find extension for " + mimeType); 277 } 278 } 279 } 280 } 281 if (extension == null) { 282 if (Constants.LOGVV) { 283 Log.v(Constants.TAG, "keeping extension"); 284 } 285 extension = filename.substring(lastDotIndex); 286 } 287 return extension; 288 } 289 290 private static boolean isFilenameAvailableLocked(File[] parents, String name) { 291 if (Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(name)) return false; 292 293 for (File parent : parents) { 294 if (new File(parent, name).exists()) { 295 return false; 296 } 297 } 298 299 return true; 300 } 301 302 private static String generateAvailableFilenameLocked( 303 File[] parents, String prefix, String suffix) throws IOException { 304 String name = prefix + suffix; 305 if (isFilenameAvailableLocked(parents, name)) { 306 return name; 307 } 308 309 /* 310 * This number is used to generate partially randomized filenames to avoid 311 * collisions. 312 * It starts at 1. 313 * The next 9 iterations increment it by 1 at a time (up to 10). 314 * The next 9 iterations increment it by 1 to 10 (random) at a time. 315 * The next 9 iterations increment it by 1 to 100 (random) at a time. 316 * ... Up to the point where it increases by 100000000 at a time. 317 * (the maximum value that can be reached is 1000000000) 318 * As soon as a number is reached that generates a filename that doesn't exist, 319 * that filename is used. 320 * If the filename coming in is [base].[ext], the generated filenames are 321 * [base]-[sequence].[ext]. 322 */ 323 int sequence = 1; 324 for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { 325 for (int iteration = 0; iteration < 9; ++iteration) { 326 name = prefix + Constants.FILENAME_SEQUENCE_SEPARATOR + sequence + suffix; 327 if (isFilenameAvailableLocked(parents, name)) { 328 return name; 329 } 330 sequence += sRandom.nextInt(magnitude) + 1; 331 } 332 } 333 334 throw new IOException("Failed to generate an available filename"); 335 } 336 337 /** 338 * Checks whether the filename looks legitimate for security purposes. This 339 * prevents us from opening files that aren't actually downloads. 340 */ 341 static boolean isFilenameValid(Context context, File file) { 342 final File[] whitelist; 343 try { 344 file = file.getCanonicalFile(); 345 whitelist = new File[] { 346 context.getFilesDir().getCanonicalFile(), 347 context.getCacheDir().getCanonicalFile(), 348 Environment.getDownloadCacheDirectory().getCanonicalFile(), 349 Environment.getExternalStorageDirectory().getCanonicalFile(), 350 }; 351 } catch (IOException e) { 352 Log.w(TAG, "Failed to resolve canonical path: " + e); 353 return false; 354 } 355 356 for (File testDir : whitelist) { 357 if (FileUtils.contains(testDir, file)) { 358 return true; 359 } 360 } 361 362 return false; 363 } 364 365 public static File getRunningDestinationDirectory(Context context, int destination) 366 throws IOException { 367 return getDestinationDirectory(context, destination, true); 368 } 369 370 public static File getSuccessDestinationDirectory(Context context, int destination) 371 throws IOException { 372 return getDestinationDirectory(context, destination, false); 373 } 374 375 private static File getDestinationDirectory(Context context, int destination, boolean running) 376 throws IOException { 377 switch (destination) { 378 case Downloads.Impl.DESTINATION_CACHE_PARTITION: 379 case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE: 380 case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING: 381 if (running) { 382 return context.getFilesDir(); 383 } else { 384 return context.getCacheDir(); 385 } 386 387 case Downloads.Impl.DESTINATION_SYSTEMCACHE_PARTITION: 388 if (running) { 389 return new File(Environment.getDownloadCacheDirectory(), 390 Constants.DIRECTORY_CACHE_RUNNING); 391 } else { 392 return Environment.getDownloadCacheDirectory(); 393 } 394 395 case Downloads.Impl.DESTINATION_EXTERNAL: 396 final File target = new File( 397 Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS); 398 if (!target.isDirectory() && target.mkdirs()) { 399 throw new IOException("unable to create external downloads directory"); 400 } 401 return target; 402 403 default: 404 throw new IllegalStateException("unexpected destination: " + destination); 405 } 406 } 407 408 /** 409 * Checks whether this looks like a legitimate selection parameter 410 */ 411 public static void validateSelection(String selection, Set<String> allowedColumns) { 412 try { 413 if (selection == null || selection.isEmpty()) { 414 return; 415 } 416 Lexer lexer = new Lexer(selection, allowedColumns); 417 parseExpression(lexer); 418 if (lexer.currentToken() != Lexer.TOKEN_END) { 419 throw new IllegalArgumentException("syntax error"); 420 } 421 } catch (RuntimeException ex) { 422 if (Constants.LOGV) { 423 Log.d(Constants.TAG, "invalid selection [" + selection + "] triggered " + ex); 424 } else if (false) { 425 Log.d(Constants.TAG, "invalid selection triggered " + ex); 426 } 427 throw ex; 428 } 429 430 } 431 432 // expression <- ( expression ) | statement [AND_OR ( expression ) | statement] * 433 // | statement [AND_OR expression]* 434 private static void parseExpression(Lexer lexer) { 435 for (;;) { 436 // ( expression ) 437 if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) { 438 lexer.advance(); 439 parseExpression(lexer); 440 if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) { 441 throw new IllegalArgumentException("syntax error, unmatched parenthese"); 442 } 443 lexer.advance(); 444 } else { 445 // statement 446 parseStatement(lexer); 447 } 448 if (lexer.currentToken() != Lexer.TOKEN_AND_OR) { 449 break; 450 } 451 lexer.advance(); 452 } 453 } 454 455 // statement <- COLUMN COMPARE VALUE 456 // | COLUMN IS NULL 457 private static void parseStatement(Lexer lexer) { 458 // both possibilities start with COLUMN 459 if (lexer.currentToken() != Lexer.TOKEN_COLUMN) { 460 throw new IllegalArgumentException("syntax error, expected column name"); 461 } 462 lexer.advance(); 463 464 // statement <- COLUMN COMPARE VALUE 465 if (lexer.currentToken() == Lexer.TOKEN_COMPARE) { 466 lexer.advance(); 467 if (lexer.currentToken() != Lexer.TOKEN_VALUE) { 468 throw new IllegalArgumentException("syntax error, expected quoted string"); 469 } 470 lexer.advance(); 471 return; 472 } 473 474 // statement <- COLUMN IS NULL 475 if (lexer.currentToken() == Lexer.TOKEN_IS) { 476 lexer.advance(); 477 if (lexer.currentToken() != Lexer.TOKEN_NULL) { 478 throw new IllegalArgumentException("syntax error, expected NULL"); 479 } 480 lexer.advance(); 481 return; 482 } 483 484 // didn't get anything good after COLUMN 485 throw new IllegalArgumentException("syntax error after column name"); 486 } 487 488 /** 489 * A simple lexer that recognizes the words of our restricted subset of SQL where clauses 490 */ 491 private static class Lexer { 492 public static final int TOKEN_START = 0; 493 public static final int TOKEN_OPEN_PAREN = 1; 494 public static final int TOKEN_CLOSE_PAREN = 2; 495 public static final int TOKEN_AND_OR = 3; 496 public static final int TOKEN_COLUMN = 4; 497 public static final int TOKEN_COMPARE = 5; 498 public static final int TOKEN_VALUE = 6; 499 public static final int TOKEN_IS = 7; 500 public static final int TOKEN_NULL = 8; 501 public static final int TOKEN_END = 9; 502 503 private final String mSelection; 504 private final Set<String> mAllowedColumns; 505 private int mOffset = 0; 506 private int mCurrentToken = TOKEN_START; 507 private final char[] mChars; 508 509 public Lexer(String selection, Set<String> allowedColumns) { 510 mSelection = selection; 511 mAllowedColumns = allowedColumns; 512 mChars = new char[mSelection.length()]; 513 mSelection.getChars(0, mChars.length, mChars, 0); 514 advance(); 515 } 516 517 public int currentToken() { 518 return mCurrentToken; 519 } 520 521 public void advance() { 522 char[] chars = mChars; 523 524 // consume whitespace 525 while (mOffset < chars.length && chars[mOffset] == ' ') { 526 ++mOffset; 527 } 528 529 // end of input 530 if (mOffset == chars.length) { 531 mCurrentToken = TOKEN_END; 532 return; 533 } 534 535 // "(" 536 if (chars[mOffset] == '(') { 537 ++mOffset; 538 mCurrentToken = TOKEN_OPEN_PAREN; 539 return; 540 } 541 542 // ")" 543 if (chars[mOffset] == ')') { 544 ++mOffset; 545 mCurrentToken = TOKEN_CLOSE_PAREN; 546 return; 547 } 548 549 // "?" 550 if (chars[mOffset] == '?') { 551 ++mOffset; 552 mCurrentToken = TOKEN_VALUE; 553 return; 554 } 555 556 // "=" and "==" 557 if (chars[mOffset] == '=') { 558 ++mOffset; 559 mCurrentToken = TOKEN_COMPARE; 560 if (mOffset < chars.length && chars[mOffset] == '=') { 561 ++mOffset; 562 } 563 return; 564 } 565 566 // ">" and ">=" 567 if (chars[mOffset] == '>') { 568 ++mOffset; 569 mCurrentToken = TOKEN_COMPARE; 570 if (mOffset < chars.length && chars[mOffset] == '=') { 571 ++mOffset; 572 } 573 return; 574 } 575 576 // "<", "<=" and "<>" 577 if (chars[mOffset] == '<') { 578 ++mOffset; 579 mCurrentToken = TOKEN_COMPARE; 580 if (mOffset < chars.length && (chars[mOffset] == '=' || chars[mOffset] == '>')) { 581 ++mOffset; 582 } 583 return; 584 } 585 586 // "!=" 587 if (chars[mOffset] == '!') { 588 ++mOffset; 589 mCurrentToken = TOKEN_COMPARE; 590 if (mOffset < chars.length && chars[mOffset] == '=') { 591 ++mOffset; 592 return; 593 } 594 throw new IllegalArgumentException("Unexpected character after !"); 595 } 596 597 // columns and keywords 598 // first look for anything that looks like an identifier or a keyword 599 // and then recognize the individual words. 600 // no attempt is made at discarding sequences of underscores with no alphanumeric 601 // characters, even though it's not clear that they'd be legal column names. 602 if (isIdentifierStart(chars[mOffset])) { 603 int startOffset = mOffset; 604 ++mOffset; 605 while (mOffset < chars.length && isIdentifierChar(chars[mOffset])) { 606 ++mOffset; 607 } 608 String word = mSelection.substring(startOffset, mOffset); 609 if (mOffset - startOffset <= 4) { 610 if (word.equals("IS")) { 611 mCurrentToken = TOKEN_IS; 612 return; 613 } 614 if (word.equals("OR") || word.equals("AND")) { 615 mCurrentToken = TOKEN_AND_OR; 616 return; 617 } 618 if (word.equals("NULL")) { 619 mCurrentToken = TOKEN_NULL; 620 return; 621 } 622 } 623 if (mAllowedColumns.contains(word)) { 624 mCurrentToken = TOKEN_COLUMN; 625 return; 626 } 627 throw new IllegalArgumentException("unrecognized column or keyword"); 628 } 629 630 // quoted strings 631 if (chars[mOffset] == '\'') { 632 ++mOffset; 633 while (mOffset < chars.length) { 634 if (chars[mOffset] == '\'') { 635 if (mOffset + 1 < chars.length && chars[mOffset + 1] == '\'') { 636 ++mOffset; 637 } else { 638 break; 639 } 640 } 641 ++mOffset; 642 } 643 if (mOffset == chars.length) { 644 throw new IllegalArgumentException("unterminated string"); 645 } 646 ++mOffset; 647 mCurrentToken = TOKEN_VALUE; 648 return; 649 } 650 651 // anything we don't recognize 652 throw new IllegalArgumentException("illegal character: " + chars[mOffset]); 653 } 654 655 private static final boolean isIdentifierStart(char c) { 656 return c == '_' || 657 (c >= 'A' && c <= 'Z') || 658 (c >= 'a' && c <= 'z'); 659 } 660 661 private static final boolean isIdentifierChar(char c) { 662 return c == '_' || 663 (c >= 'A' && c <= 'Z') || 664 (c >= 'a' && c <= 'z') || 665 (c >= '0' && c <= '9'); 666 } 667 } 668 669 /** 670 * Replace invalid filename characters according to 671 * specifications of the VFAT. 672 * @note Package-private due to testing. 673 */ 674 private static String replaceInvalidVfatCharacters(String filename) { 675 final char START_CTRLCODE = 0x00; 676 final char END_CTRLCODE = 0x1f; 677 final char QUOTEDBL = 0x22; 678 final char ASTERISK = 0x2A; 679 final char SLASH = 0x2F; 680 final char COLON = 0x3A; 681 final char LESS = 0x3C; 682 final char GREATER = 0x3E; 683 final char QUESTION = 0x3F; 684 final char BACKSLASH = 0x5C; 685 final char BAR = 0x7C; 686 final char DEL = 0x7F; 687 final char UNDERSCORE = 0x5F; 688 689 StringBuffer sb = new StringBuffer(); 690 char ch; 691 boolean isRepetition = false; 692 for (int i = 0; i < filename.length(); i++) { 693 ch = filename.charAt(i); 694 if ((START_CTRLCODE <= ch && 695 ch <= END_CTRLCODE) || 696 ch == QUOTEDBL || 697 ch == ASTERISK || 698 ch == SLASH || 699 ch == COLON || 700 ch == LESS || 701 ch == GREATER || 702 ch == QUESTION || 703 ch == BACKSLASH || 704 ch == BAR || 705 ch == DEL){ 706 if (!isRepetition) { 707 sb.append(UNDERSCORE); 708 isRepetition = true; 709 } 710 } else { 711 sb.append(ch); 712 isRepetition = false; 713 } 714 } 715 return sb.toString(); 716 } 717} 718