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