• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
setPermissions(File path, int mode, int uid, int gid)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      */
setPermissions(String path, int mode, int uid, int gid)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      */
setPermissions(FileDescriptor fd, int mode, int uid, int gid)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 
copyPermissions(File from, File to)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      */
getUid(String path)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      */
sync(FileOutputStream stream)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
copyFile(File srcFile, File destFile)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
copyFileOrThrow(File srcFile, File destFile)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
copyToFile(InputStream inputStream, File destFile)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      */
copyToFileOrThrow(InputStream inputStream, File destFile)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      */
isFilenameSafe(File file)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      */
readTextFile(File file, int max, String ellipsis)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 
stringToFile(File file, String string)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      */
stringToFile(String filename, String string)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      */
checksumCrc32(File file)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      */
deleteOlderFiles(File dir, int minCount, long minAge)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      */
contains(File[] dirs, File file)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      */
contains(File dir, File file)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 
deleteContentsAndDir(File dir)445     public static boolean deleteContentsAndDir(File dir) {
446         if (deleteContents(dir)) {
447             return dir.delete();
448         } else {
449             return false;
450         }
451     }
452 
deleteContents(File dir)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 
isValidExtFilenameChar(char c)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      */
isValidExtFilename(String name)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      */
buildValidExtFilename(String name)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 
isValidFatFilenameChar(char c)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      */
isValidFatFilename(String name)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      */
buildValidFatFilename(String name)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
trimFilename(String str, int maxBytes)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 
trimFilename(StringBuilder res, int maxBytes)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 
rewriteAfterRename(File beforeDir, File afterDir, String path)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 
rewriteAfterRename(File beforeDir, File afterDir, String[] paths)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      */
rewriteAfterRename(File beforeDir, File afterDir, File file)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      */
buildUniqueFile(File parent, String mimeType, String displayName)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      */
splitFileName(String mimeType, String displayName)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 
buildFile(File parent, String name, String ext)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 
listOrEmpty(@ullable File dir)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 
listFilesOrEmpty(@ullable File dir)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 
listFilesOrEmpty(@ullable File dir, FilenameFilter filter)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 
newFileOrNull(@ullable String path)727     public static @Nullable File newFileOrNull(@Nullable String path) {
728         return (path != null) ? new File(path) : null;
729     }
730 }
731