Home | History | Annotate | Download | only in downloads
      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.content.Intent;
     21 import android.content.pm.PackageManager;
     22 import android.content.pm.ResolveInfo;
     23 import android.net.NetworkInfo;
     24 import android.net.Uri;
     25 import android.os.Environment;
     26 import android.os.SystemClock;
     27 import android.provider.Downloads;
     28 import android.util.Log;
     29 import android.webkit.MimeTypeMap;
     30 
     31 import java.io.File;
     32 import java.util.Random;
     33 import java.util.Set;
     34 import java.util.regex.Matcher;
     35 import java.util.regex.Pattern;
     36 
     37 /**
     38  * Some helper functions for the download manager
     39  */
     40 public 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