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