Home | History | Annotate | Download | only in os
      1 /*
      2  * Copyright (C) 2006 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 android.os;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.provider.DocumentsContract.Document;
     22 import android.system.ErrnoException;
     23 import android.system.Os;
     24 import android.system.StructStat;
     25 import android.text.TextUtils;
     26 import android.util.Log;
     27 import android.util.Slog;
     28 import android.webkit.MimeTypeMap;
     29 
     30 import com.android.internal.annotations.VisibleForTesting;
     31 
     32 import libcore.util.EmptyArray;
     33 
     34 import java.io.BufferedInputStream;
     35 import java.io.ByteArrayOutputStream;
     36 import java.io.File;
     37 import java.io.FileDescriptor;
     38 import java.io.FileInputStream;
     39 import java.io.FileNotFoundException;
     40 import java.io.FileOutputStream;
     41 import java.io.FileWriter;
     42 import java.io.FilenameFilter;
     43 import java.io.IOException;
     44 import java.io.InputStream;
     45 import java.nio.charset.StandardCharsets;
     46 import java.util.Arrays;
     47 import java.util.Comparator;
     48 import java.util.Objects;
     49 import java.util.regex.Pattern;
     50 import java.util.zip.CRC32;
     51 import java.util.zip.CheckedInputStream;
     52 
     53 /**
     54  * Tools for managing files.  Not for public consumption.
     55  * @hide
     56  */
     57 public class FileUtils {
     58     private static final String TAG = "FileUtils";
     59 
     60     public static final int S_IRWXU = 00700;
     61     public static final int S_IRUSR = 00400;
     62     public static final int S_IWUSR = 00200;
     63     public static final int S_IXUSR = 00100;
     64 
     65     public static final int S_IRWXG = 00070;
     66     public static final int S_IRGRP = 00040;
     67     public static final int S_IWGRP = 00020;
     68     public static final int S_IXGRP = 00010;
     69 
     70     public static final int S_IRWXO = 00007;
     71     public static final int S_IROTH = 00004;
     72     public static final int S_IWOTH = 00002;
     73     public static final int S_IXOTH = 00001;
     74 
     75     /** Regular expression for safe filenames: no spaces or metacharacters.
     76       *
     77       * Use a preload holder so that FileUtils can be compile-time initialized.
     78       */
     79     private static class NoImagePreloadHolder {
     80         public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+");
     81     }
     82 
     83     private static final File[] EMPTY = new File[0];
     84 
     85     /**
     86      * Set owner and mode of of given {@link File}.
     87      *
     88      * @param mode to apply through {@code chmod}
     89      * @param uid to apply through {@code chown}, or -1 to leave unchanged
     90      * @param gid to apply through {@code chown}, or -1 to leave unchanged
     91      * @return 0 on success, otherwise errno.
     92      */
     93     public static int setPermissions(File path, int mode, int uid, int gid) {
     94         return setPermissions(path.getAbsolutePath(), mode, uid, gid);
     95     }
     96 
     97     /**
     98      * Set owner and mode of of given path.
     99      *
    100      * @param mode to apply through {@code chmod}
    101      * @param uid to apply through {@code chown}, or -1 to leave unchanged
    102      * @param gid to apply through {@code chown}, or -1 to leave unchanged
    103      * @return 0 on success, otherwise errno.
    104      */
    105     public static int setPermissions(String path, int mode, int uid, int gid) {
    106         try {
    107             Os.chmod(path, mode);
    108         } catch (ErrnoException e) {
    109             Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
    110             return e.errno;
    111         }
    112 
    113         if (uid >= 0 || gid >= 0) {
    114             try {
    115                 Os.chown(path, uid, gid);
    116             } catch (ErrnoException e) {
    117                 Slog.w(TAG, "Failed to chown(" + path + "): " + e);
    118                 return e.errno;
    119             }
    120         }
    121 
    122         return 0;
    123     }
    124 
    125     /**
    126      * Set owner and mode of of given {@link FileDescriptor}.
    127      *
    128      * @param mode to apply through {@code chmod}
    129      * @param uid to apply through {@code chown}, or -1 to leave unchanged
    130      * @param gid to apply through {@code chown}, or -1 to leave unchanged
    131      * @return 0 on success, otherwise errno.
    132      */
    133     public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) {
    134         try {
    135             Os.fchmod(fd, mode);
    136         } catch (ErrnoException e) {
    137             Slog.w(TAG, "Failed to fchmod(): " + e);
    138             return e.errno;
    139         }
    140 
    141         if (uid >= 0 || gid >= 0) {
    142             try {
    143                 Os.fchown(fd, uid, gid);
    144             } catch (ErrnoException e) {
    145                 Slog.w(TAG, "Failed to fchown(): " + e);
    146                 return e.errno;
    147             }
    148         }
    149 
    150         return 0;
    151     }
    152 
    153     public static void copyPermissions(File from, File to) throws IOException {
    154         try {
    155             final StructStat stat = Os.stat(from.getAbsolutePath());
    156             Os.chmod(to.getAbsolutePath(), stat.st_mode);
    157             Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid);
    158         } catch (ErrnoException e) {
    159             throw e.rethrowAsIOException();
    160         }
    161     }
    162 
    163     /**
    164      * Return owning UID of given path, otherwise -1.
    165      */
    166     public static int getUid(String path) {
    167         try {
    168             return Os.stat(path).st_uid;
    169         } catch (ErrnoException e) {
    170             return -1;
    171         }
    172     }
    173 
    174     /**
    175      * Perform an fsync on the given FileOutputStream.  The stream at this
    176      * point must be flushed but not yet closed.
    177      */
    178     public static boolean sync(FileOutputStream stream) {
    179         try {
    180             if (stream != null) {
    181                 stream.getFD().sync();
    182             }
    183             return true;
    184         } catch (IOException e) {
    185         }
    186         return false;
    187     }
    188 
    189     @Deprecated
    190     public static boolean copyFile(File srcFile, File destFile) {
    191         try {
    192             copyFileOrThrow(srcFile, destFile);
    193             return true;
    194         } catch (IOException e) {
    195             return false;
    196         }
    197     }
    198 
    199     // copy a file from srcFile to destFile, return true if succeed, return
    200     // false if fail
    201     public static void copyFileOrThrow(File srcFile, File destFile) throws IOException {
    202         try (InputStream in = new FileInputStream(srcFile)) {
    203             copyToFileOrThrow(in, destFile);
    204         }
    205     }
    206 
    207     @Deprecated
    208     public static boolean copyToFile(InputStream inputStream, File destFile) {
    209         try {
    210             copyToFileOrThrow(inputStream, destFile);
    211             return true;
    212         } catch (IOException e) {
    213             return false;
    214         }
    215     }
    216 
    217     /**
    218      * Copy data from a source stream to destFile.
    219      * Return true if succeed, return false if failed.
    220      */
    221     public static void copyToFileOrThrow(InputStream inputStream, File destFile)
    222             throws IOException {
    223         if (destFile.exists()) {
    224             destFile.delete();
    225         }
    226         FileOutputStream out = new FileOutputStream(destFile);
    227         try {
    228             byte[] buffer = new byte[4096];
    229             int bytesRead;
    230             while ((bytesRead = inputStream.read(buffer)) >= 0) {
    231                 out.write(buffer, 0, bytesRead);
    232             }
    233         } finally {
    234             out.flush();
    235             try {
    236                 out.getFD().sync();
    237             } catch (IOException e) {
    238             }
    239             out.close();
    240         }
    241     }
    242 
    243     /**
    244      * Check if a filename is "safe" (no metacharacters or spaces).
    245      * @param file  The file to check
    246      */
    247     public static boolean isFilenameSafe(File file) {
    248         // Note, we check whether it matches what's known to be safe,
    249         // rather than what's known to be unsafe.  Non-ASCII, control
    250         // characters, etc. are all unsafe by default.
    251         return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches();
    252     }
    253 
    254     /**
    255      * Read a text file into a String, optionally limiting the length.
    256      * @param file to read (will not seek, so things like /proc files are OK)
    257      * @param max length (positive for head, negative of tail, 0 for no limit)
    258      * @param ellipsis to add of the file was truncated (can be null)
    259      * @return the contents of the file, possibly truncated
    260      * @throws IOException if something goes wrong reading the file
    261      */
    262     public static String readTextFile(File file, int max, String ellipsis) throws IOException {
    263         InputStream input = new FileInputStream(file);
    264         // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
    265         // input stream, bytes read not equal to buffer size is not necessarily the correct
    266         // indication for EOF; but it is true for BufferedInputStream due to its implementation.
    267         BufferedInputStream bis = new BufferedInputStream(input);
    268         try {
    269             long size = file.length();
    270             if (max > 0 || (size > 0 && max == 0)) {  // "head" mode: read the first N bytes
    271                 if (size > 0 && (max == 0 || size < max)) max = (int) size;
    272                 byte[] data = new byte[max + 1];
    273                 int length = bis.read(data);
    274                 if (length <= 0) return "";
    275                 if (length <= max) return new String(data, 0, length);
    276                 if (ellipsis == null) return new String(data, 0, max);
    277                 return new String(data, 0, max) + ellipsis;
    278             } else if (max < 0) {  // "tail" mode: keep the last N
    279                 int len;
    280                 boolean rolled = false;
    281                 byte[] last = null;
    282                 byte[] data = null;
    283                 do {
    284                     if (last != null) rolled = true;
    285                     byte[] tmp = last; last = data; data = tmp;
    286                     if (data == null) data = new byte[-max];
    287                     len = bis.read(data);
    288                 } while (len == data.length);
    289 
    290                 if (last == null && len <= 0) return "";
    291                 if (last == null) return new String(data, 0, len);
    292                 if (len > 0) {
    293                     rolled = true;
    294                     System.arraycopy(last, len, last, 0, last.length - len);
    295                     System.arraycopy(data, 0, last, last.length - len, len);
    296                 }
    297                 if (ellipsis == null || !rolled) return new String(last);
    298                 return ellipsis + new String(last);
    299             } else {  // "cat" mode: size unknown, read it all in streaming fashion
    300                 ByteArrayOutputStream contents = new ByteArrayOutputStream();
    301                 int len;
    302                 byte[] data = new byte[1024];
    303                 do {
    304                     len = bis.read(data);
    305                     if (len > 0) contents.write(data, 0, len);
    306                 } while (len == data.length);
    307                 return contents.toString();
    308             }
    309         } finally {
    310             bis.close();
    311             input.close();
    312         }
    313     }
    314 
    315     public static void stringToFile(File file, String string) throws IOException {
    316         stringToFile(file.getAbsolutePath(), string);
    317     }
    318 
    319     /**
    320      * Writes string to file. Basically same as "echo -n $string > $filename"
    321      *
    322      * @param filename
    323      * @param string
    324      * @throws IOException
    325      */
    326     public static void stringToFile(String filename, String string) throws IOException {
    327         FileWriter out = new FileWriter(filename);
    328         try {
    329             out.write(string);
    330         } finally {
    331             out.close();
    332         }
    333     }
    334 
    335     /**
    336      * Computes the checksum of a file using the CRC32 checksum routine.
    337      * The value of the checksum is returned.
    338      *
    339      * @param file  the file to checksum, must not be null
    340      * @return the checksum value or an exception is thrown.
    341      */
    342     public static long checksumCrc32(File file) throws FileNotFoundException, IOException {
    343         CRC32 checkSummer = new CRC32();
    344         CheckedInputStream cis = null;
    345 
    346         try {
    347             cis = new CheckedInputStream( new FileInputStream(file), checkSummer);
    348             byte[] buf = new byte[128];
    349             while(cis.read(buf) >= 0) {
    350                 // Just read for checksum to get calculated.
    351             }
    352             return checkSummer.getValue();
    353         } finally {
    354             if (cis != null) {
    355                 try {
    356                     cis.close();
    357                 } catch (IOException e) {
    358                 }
    359             }
    360         }
    361     }
    362 
    363     /**
    364      * Delete older files in a directory until only those matching the given
    365      * constraints remain.
    366      *
    367      * @param minCount Always keep at least this many files.
    368      * @param minAge Always keep files younger than this age.
    369      * @return if any files were deleted.
    370      */
    371     public static boolean deleteOlderFiles(File dir, int minCount, long minAge) {
    372         if (minCount < 0 || minAge < 0) {
    373             throw new IllegalArgumentException("Constraints must be positive or 0");
    374         }
    375 
    376         final File[] files = dir.listFiles();
    377         if (files == null) return false;
    378 
    379         // Sort with newest files first
    380         Arrays.sort(files, new Comparator<File>() {
    381             @Override
    382             public int compare(File lhs, File rhs) {
    383                 return (int) (rhs.lastModified() - lhs.lastModified());
    384             }
    385         });
    386 
    387         // Keep at least minCount files
    388         boolean deleted = false;
    389         for (int i = minCount; i < files.length; i++) {
    390             final File file = files[i];
    391 
    392             // Keep files newer than minAge
    393             final long age = System.currentTimeMillis() - file.lastModified();
    394             if (age > minAge) {
    395                 if (file.delete()) {
    396                     Log.d(TAG, "Deleted old file " + file);
    397                     deleted = true;
    398                 }
    399             }
    400         }
    401         return deleted;
    402     }
    403 
    404     /**
    405      * Test if a file lives under the given directory, either as a direct child
    406      * or a distant grandchild.
    407      * <p>
    408      * Both files <em>must</em> have been resolved using
    409      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
    410      * attacks.
    411      */
    412     public static boolean contains(File[] dirs, File file) {
    413         for (File dir : dirs) {
    414             if (contains(dir, file)) {
    415                 return true;
    416             }
    417         }
    418         return false;
    419     }
    420 
    421     /**
    422      * Test if a file lives under the given directory, either as a direct child
    423      * or a distant grandchild.
    424      * <p>
    425      * Both files <em>must</em> have been resolved using
    426      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
    427      * attacks.
    428      */
    429     public static boolean contains(File dir, File file) {
    430         if (dir == null || file == null) return false;
    431 
    432         String dirPath = dir.getAbsolutePath();
    433         String filePath = file.getAbsolutePath();
    434 
    435         if (dirPath.equals(filePath)) {
    436             return true;
    437         }
    438 
    439         if (!dirPath.endsWith("/")) {
    440             dirPath += "/";
    441         }
    442         return filePath.startsWith(dirPath);
    443     }
    444 
    445     public static boolean deleteContentsAndDir(File dir) {
    446         if (deleteContents(dir)) {
    447             return dir.delete();
    448         } else {
    449             return false;
    450         }
    451     }
    452 
    453     public static boolean deleteContents(File dir) {
    454         File[] files = dir.listFiles();
    455         boolean success = true;
    456         if (files != null) {
    457             for (File file : files) {
    458                 if (file.isDirectory()) {
    459                     success &= deleteContents(file);
    460                 }
    461                 if (!file.delete()) {
    462                     Log.w(TAG, "Failed to delete " + file);
    463                     success = false;
    464                 }
    465             }
    466         }
    467         return success;
    468     }
    469 
    470     private static boolean isValidExtFilenameChar(char c) {
    471         switch (c) {
    472             case '\0':
    473             case '/':
    474                 return false;
    475             default:
    476                 return true;
    477         }
    478     }
    479 
    480     /**
    481      * Check if given filename is valid for an ext4 filesystem.
    482      */
    483     public static boolean isValidExtFilename(String name) {
    484         return (name != null) && name.equals(buildValidExtFilename(name));
    485     }
    486 
    487     /**
    488      * Mutate the given filename to make it valid for an ext4 filesystem,
    489      * replacing any invalid characters with "_".
    490      */
    491     public static String buildValidExtFilename(String name) {
    492         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
    493             return "(invalid)";
    494         }
    495         final StringBuilder res = new StringBuilder(name.length());
    496         for (int i = 0; i < name.length(); i++) {
    497             final char c = name.charAt(i);
    498             if (isValidExtFilenameChar(c)) {
    499                 res.append(c);
    500             } else {
    501                 res.append('_');
    502             }
    503         }
    504         trimFilename(res, 255);
    505         return res.toString();
    506     }
    507 
    508     private static boolean isValidFatFilenameChar(char c) {
    509         if ((0x00 <= c && c <= 0x1f)) {
    510             return false;
    511         }
    512         switch (c) {
    513             case '"':
    514             case '*':
    515             case '/':
    516             case ':':
    517             case '<':
    518             case '>':
    519             case '?':
    520             case '\\':
    521             case '|':
    522             case 0x7F:
    523                 return false;
    524             default:
    525                 return true;
    526         }
    527     }
    528 
    529     /**
    530      * Check if given filename is valid for a FAT filesystem.
    531      */
    532     public static boolean isValidFatFilename(String name) {
    533         return (name != null) && name.equals(buildValidFatFilename(name));
    534     }
    535 
    536     /**
    537      * Mutate the given filename to make it valid for a FAT filesystem,
    538      * replacing any invalid characters with "_".
    539      */
    540     public static String buildValidFatFilename(String name) {
    541         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
    542             return "(invalid)";
    543         }
    544         final StringBuilder res = new StringBuilder(name.length());
    545         for (int i = 0; i < name.length(); i++) {
    546             final char c = name.charAt(i);
    547             if (isValidFatFilenameChar(c)) {
    548                 res.append(c);
    549             } else {
    550                 res.append('_');
    551             }
    552         }
    553         // Even though vfat allows 255 UCS-2 chars, we might eventually write to
    554         // ext4 through a FUSE layer, so use that limit.
    555         trimFilename(res, 255);
    556         return res.toString();
    557     }
    558 
    559     @VisibleForTesting
    560     public static String trimFilename(String str, int maxBytes) {
    561         final StringBuilder res = new StringBuilder(str);
    562         trimFilename(res, maxBytes);
    563         return res.toString();
    564     }
    565 
    566     private static void trimFilename(StringBuilder res, int maxBytes) {
    567         byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
    568         if (raw.length > maxBytes) {
    569             maxBytes -= 3;
    570             while (raw.length > maxBytes) {
    571                 res.deleteCharAt(res.length() / 2);
    572                 raw = res.toString().getBytes(StandardCharsets.UTF_8);
    573             }
    574             res.insert(res.length() / 2, "...");
    575         }
    576     }
    577 
    578     public static String rewriteAfterRename(File beforeDir, File afterDir, String path) {
    579         if (path == null) return null;
    580         final File result = rewriteAfterRename(beforeDir, afterDir, new File(path));
    581         return (result != null) ? result.getAbsolutePath() : null;
    582     }
    583 
    584     public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) {
    585         if (paths == null) return null;
    586         final String[] result = new String[paths.length];
    587         for (int i = 0; i < paths.length; i++) {
    588             result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]);
    589         }
    590         return result;
    591     }
    592 
    593     /**
    594      * Given a path under the "before" directory, rewrite it to live under the
    595      * "after" directory. For example, {@code /before/foo/bar.txt} would become
    596      * {@code /after/foo/bar.txt}.
    597      */
    598     public static File rewriteAfterRename(File beforeDir, File afterDir, File file) {
    599         if (file == null || beforeDir == null || afterDir == null) return null;
    600         if (contains(beforeDir, file)) {
    601             final String splice = file.getAbsolutePath().substring(
    602                     beforeDir.getAbsolutePath().length());
    603             return new File(afterDir, splice);
    604         }
    605         return null;
    606     }
    607 
    608     /**
    609      * Generates a unique file name under the given parent directory. If the display name doesn't
    610      * have an extension that matches the requested MIME type, the default extension for that MIME
    611      * type is appended. If a file already exists, the name is appended with a numerical value to
    612      * make it unique.
    613      *
    614      * For example, the display name 'example' with 'text/plain' MIME might produce
    615      * 'example.txt' or 'example (1).txt', etc.
    616      *
    617      * @throws FileNotFoundException
    618      */
    619     public static File buildUniqueFile(File parent, String mimeType, String displayName)
    620             throws FileNotFoundException {
    621         final String[] parts = splitFileName(mimeType, displayName);
    622         final String name = parts[0];
    623         final String ext = parts[1];
    624         File file = buildFile(parent, name, ext);
    625 
    626         // If conflicting file, try adding counter suffix
    627         int n = 0;
    628         while (file.exists()) {
    629             if (n++ >= 32) {
    630                 throw new FileNotFoundException("Failed to create unique file");
    631             }
    632             file = buildFile(parent, name + " (" + n + ")", ext);
    633         }
    634 
    635         return file;
    636     }
    637 
    638     /**
    639      * Splits file name into base name and extension.
    640      * If the display name doesn't have an extension that matches the requested MIME type, the
    641      * extension is regarded as a part of filename and default extension for that MIME type is
    642      * appended.
    643      */
    644     public static String[] splitFileName(String mimeType, String displayName) {
    645         String name;
    646         String ext;
    647 
    648         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
    649             name = displayName;
    650             ext = null;
    651         } else {
    652             String mimeTypeFromExt;
    653 
    654             // Extract requested extension from display name
    655             final int lastDot = displayName.lastIndexOf('.');
    656             if (lastDot >= 0) {
    657                 name = displayName.substring(0, lastDot);
    658                 ext = displayName.substring(lastDot + 1);
    659                 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
    660                         ext.toLowerCase());
    661             } else {
    662                 name = displayName;
    663                 ext = null;
    664                 mimeTypeFromExt = null;
    665             }
    666 
    667             if (mimeTypeFromExt == null) {
    668                 mimeTypeFromExt = "application/octet-stream";
    669             }
    670 
    671             final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(
    672                     mimeType);
    673             if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) {
    674                 // Extension maps back to requested MIME type; allow it
    675             } else {
    676                 // No match; insist that create file matches requested MIME
    677                 name = displayName;
    678                 ext = extFromMimeType;
    679             }
    680         }
    681 
    682         if (ext == null) {
    683             ext = "";
    684         }
    685 
    686         return new String[] { name, ext };
    687     }
    688 
    689     private static File buildFile(File parent, String name, String ext) {
    690         if (TextUtils.isEmpty(ext)) {
    691             return new File(parent, name);
    692         } else {
    693             return new File(parent, name + "." + ext);
    694         }
    695     }
    696 
    697     public static @NonNull String[] listOrEmpty(@Nullable File dir) {
    698         if (dir == null) return EmptyArray.STRING;
    699         final String[] res = dir.list();
    700         if (res != null) {
    701             return res;
    702         } else {
    703             return EmptyArray.STRING;
    704         }
    705     }
    706 
    707     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
    708         if (dir == null) return EMPTY;
    709         final File[] res = dir.listFiles();
    710         if (res != null) {
    711             return res;
    712         } else {
    713             return EMPTY;
    714         }
    715     }
    716 
    717     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) {
    718         if (dir == null) return EMPTY;
    719         final File[] res = dir.listFiles(filter);
    720         if (res != null) {
    721             return res;
    722         } else {
    723             return EMPTY;
    724         }
    725     }
    726 
    727     public static @Nullable File newFileOrNull(@Nullable String path) {
    728         return (path != null) ? new File(path) : null;
    729     }
    730 }
    731