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 static android.system.OsConstants.SPLICE_F_MORE;
20 import static android.system.OsConstants.SPLICE_F_MOVE;
21 import static android.system.OsConstants.S_ISFIFO;
22 import static android.system.OsConstants.S_ISREG;
23 
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.provider.DocumentsContract.Document;
27 import android.system.ErrnoException;
28 import android.system.Os;
29 import android.system.StructStat;
30 import android.text.TextUtils;
31 import android.util.Log;
32 import android.util.Slog;
33 import android.webkit.MimeTypeMap;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.SizedInputStream;
37 
38 import libcore.io.IoUtils;
39 import libcore.util.EmptyArray;
40 
41 import java.io.BufferedInputStream;
42 import java.io.ByteArrayOutputStream;
43 import java.io.File;
44 import java.io.FileDescriptor;
45 import java.io.FileInputStream;
46 import java.io.FileNotFoundException;
47 import java.io.FileOutputStream;
48 import java.io.FilenameFilter;
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.io.OutputStream;
52 import java.nio.charset.StandardCharsets;
53 import java.util.Arrays;
54 import java.util.Comparator;
55 import java.util.Objects;
56 import java.util.concurrent.TimeUnit;
57 import java.util.regex.Pattern;
58 import java.util.zip.CRC32;
59 import java.util.zip.CheckedInputStream;
60 
61 /**
62  * Tools for managing files.  Not for public consumption.
63  * @hide
64  */
65 public class FileUtils {
66     private static final String TAG = "FileUtils";
67 
68     public static final int S_IRWXU = 00700;
69     public static final int S_IRUSR = 00400;
70     public static final int S_IWUSR = 00200;
71     public static final int S_IXUSR = 00100;
72 
73     public static final int S_IRWXG = 00070;
74     public static final int S_IRGRP = 00040;
75     public static final int S_IWGRP = 00020;
76     public static final int S_IXGRP = 00010;
77 
78     public static final int S_IRWXO = 00007;
79     public static final int S_IROTH = 00004;
80     public static final int S_IWOTH = 00002;
81     public static final int S_IXOTH = 00001;
82 
83     /** Regular expression for safe filenames: no spaces or metacharacters.
84       *
85       * Use a preload holder so that FileUtils can be compile-time initialized.
86       */
87     private static class NoImagePreloadHolder {
88         public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+");
89     }
90 
91     private static final File[] EMPTY = new File[0];
92 
93     private static final boolean ENABLE_COPY_OPTIMIZATIONS = true;
94 
95     private static final long COPY_CHECKPOINT_BYTES = 524288;
96 
97     public interface ProgressListener {
onProgress(long progress)98         public void onProgress(long progress);
99     }
100 
101     /**
102      * Set owner and mode of of given {@link File}.
103      *
104      * @param mode to apply through {@code chmod}
105      * @param uid to apply through {@code chown}, or -1 to leave unchanged
106      * @param gid to apply through {@code chown}, or -1 to leave unchanged
107      * @return 0 on success, otherwise errno.
108      */
setPermissions(File path, int mode, int uid, int gid)109     public static int setPermissions(File path, int mode, int uid, int gid) {
110         return setPermissions(path.getAbsolutePath(), mode, uid, gid);
111     }
112 
113     /**
114      * Set owner and mode of of given path.
115      *
116      * @param mode to apply through {@code chmod}
117      * @param uid to apply through {@code chown}, or -1 to leave unchanged
118      * @param gid to apply through {@code chown}, or -1 to leave unchanged
119      * @return 0 on success, otherwise errno.
120      */
setPermissions(String path, int mode, int uid, int gid)121     public static int setPermissions(String path, int mode, int uid, int gid) {
122         try {
123             Os.chmod(path, mode);
124         } catch (ErrnoException e) {
125             Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
126             return e.errno;
127         }
128 
129         if (uid >= 0 || gid >= 0) {
130             try {
131                 Os.chown(path, uid, gid);
132             } catch (ErrnoException e) {
133                 Slog.w(TAG, "Failed to chown(" + path + "): " + e);
134                 return e.errno;
135             }
136         }
137 
138         return 0;
139     }
140 
141     /**
142      * Set owner and mode of of given {@link FileDescriptor}.
143      *
144      * @param mode to apply through {@code chmod}
145      * @param uid to apply through {@code chown}, or -1 to leave unchanged
146      * @param gid to apply through {@code chown}, or -1 to leave unchanged
147      * @return 0 on success, otherwise errno.
148      */
setPermissions(FileDescriptor fd, int mode, int uid, int gid)149     public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) {
150         try {
151             Os.fchmod(fd, mode);
152         } catch (ErrnoException e) {
153             Slog.w(TAG, "Failed to fchmod(): " + e);
154             return e.errno;
155         }
156 
157         if (uid >= 0 || gid >= 0) {
158             try {
159                 Os.fchown(fd, uid, gid);
160             } catch (ErrnoException e) {
161                 Slog.w(TAG, "Failed to fchown(): " + e);
162                 return e.errno;
163             }
164         }
165 
166         return 0;
167     }
168 
copyPermissions(File from, File to)169     public static void copyPermissions(File from, File to) throws IOException {
170         try {
171             final StructStat stat = Os.stat(from.getAbsolutePath());
172             Os.chmod(to.getAbsolutePath(), stat.st_mode);
173             Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid);
174         } catch (ErrnoException e) {
175             throw e.rethrowAsIOException();
176         }
177     }
178 
179     /**
180      * Return owning UID of given path, otherwise -1.
181      */
getUid(String path)182     public static int getUid(String path) {
183         try {
184             return Os.stat(path).st_uid;
185         } catch (ErrnoException e) {
186             return -1;
187         }
188     }
189 
190     /**
191      * Perform an fsync on the given FileOutputStream.  The stream at this
192      * point must be flushed but not yet closed.
193      */
sync(FileOutputStream stream)194     public static boolean sync(FileOutputStream stream) {
195         try {
196             if (stream != null) {
197                 stream.getFD().sync();
198             }
199             return true;
200         } catch (IOException e) {
201         }
202         return false;
203     }
204 
205     /**
206      * @deprecated use {@link #copy(File, File)} instead.
207      */
208     @Deprecated
copyFile(File srcFile, File destFile)209     public static boolean copyFile(File srcFile, File destFile) {
210         try {
211             copyFileOrThrow(srcFile, destFile);
212             return true;
213         } catch (IOException e) {
214             return false;
215         }
216     }
217 
218     /**
219      * @deprecated use {@link #copy(File, File)} instead.
220      */
221     @Deprecated
copyFileOrThrow(File srcFile, File destFile)222     public static void copyFileOrThrow(File srcFile, File destFile) throws IOException {
223         try (InputStream in = new FileInputStream(srcFile)) {
224             copyToFileOrThrow(in, destFile);
225         }
226     }
227 
228     /**
229      * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
230      */
231     @Deprecated
copyToFile(InputStream inputStream, File destFile)232     public static boolean copyToFile(InputStream inputStream, File destFile) {
233         try {
234             copyToFileOrThrow(inputStream, destFile);
235             return true;
236         } catch (IOException e) {
237             return false;
238         }
239     }
240 
241     /**
242      * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
243      */
244     @Deprecated
copyToFileOrThrow(InputStream in, File destFile)245     public static void copyToFileOrThrow(InputStream in, File destFile) throws IOException {
246         if (destFile.exists()) {
247             destFile.delete();
248         }
249         try (FileOutputStream out = new FileOutputStream(destFile)) {
250             copy(in, out);
251             try {
252                 Os.fsync(out.getFD());
253             } catch (ErrnoException e) {
254                 throw e.rethrowAsIOException();
255             }
256         }
257     }
258 
259     /**
260      * Copy the contents of one file to another, replacing any existing content.
261      * <p>
262      * Attempts to use several optimization strategies to copy the data in the
263      * kernel before falling back to a userspace copy as a last resort.
264      *
265      * @return number of bytes copied.
266      */
copy(@onNull File from, @NonNull File to)267     public static long copy(@NonNull File from, @NonNull File to) throws IOException {
268         return copy(from, to, null, null);
269     }
270 
271     /**
272      * Copy the contents of one file to another, replacing any existing content.
273      * <p>
274      * Attempts to use several optimization strategies to copy the data in the
275      * kernel before falling back to a userspace copy as a last resort.
276      *
277      * @param listener to be periodically notified as the copy progresses.
278      * @param signal to signal if the copy should be cancelled early.
279      * @return number of bytes copied.
280      */
copy(@onNull File from, @NonNull File to, @Nullable ProgressListener listener, @Nullable CancellationSignal signal)281     public static long copy(@NonNull File from, @NonNull File to,
282             @Nullable ProgressListener listener, @Nullable CancellationSignal signal)
283             throws IOException {
284         try (FileInputStream in = new FileInputStream(from);
285                 FileOutputStream out = new FileOutputStream(to)) {
286             return copy(in, out, listener, signal);
287         }
288     }
289 
290     /**
291      * Copy the contents of one stream to another.
292      * <p>
293      * Attempts to use several optimization strategies to copy the data in the
294      * kernel before falling back to a userspace copy as a last resort.
295      *
296      * @return number of bytes copied.
297      */
copy(@onNull InputStream in, @NonNull OutputStream out)298     public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
299         return copy(in, out, null, null);
300     }
301 
302     /**
303      * Copy the contents of one stream to another.
304      * <p>
305      * Attempts to use several optimization strategies to copy the data in the
306      * kernel before falling back to a userspace copy as a last resort.
307      *
308      * @param listener to be periodically notified as the copy progresses.
309      * @param signal to signal if the copy should be cancelled early.
310      * @return number of bytes copied.
311      */
copy(@onNull InputStream in, @NonNull OutputStream out, @Nullable ProgressListener listener, @Nullable CancellationSignal signal)312     public static long copy(@NonNull InputStream in, @NonNull OutputStream out,
313             @Nullable ProgressListener listener, @Nullable CancellationSignal signal)
314             throws IOException {
315         if (ENABLE_COPY_OPTIMIZATIONS) {
316             if (in instanceof FileInputStream && out instanceof FileOutputStream) {
317                 return copy(((FileInputStream) in).getFD(), ((FileOutputStream) out).getFD(),
318                         listener, signal);
319             }
320         }
321 
322         // Worse case fallback to userspace
323         return copyInternalUserspace(in, out, listener, signal);
324     }
325 
326     /**
327      * Copy the contents of one FD to another.
328      * <p>
329      * Attempts to use several optimization strategies to copy the data in the
330      * kernel before falling back to a userspace copy as a last resort.
331      *
332      * @return number of bytes copied.
333      */
copy(@onNull FileDescriptor in, @NonNull FileDescriptor out)334     public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out)
335             throws IOException {
336         return copy(in, out, null, null);
337     }
338 
339     /**
340      * Copy the contents of one FD to another.
341      * <p>
342      * Attempts to use several optimization strategies to copy the data in the
343      * kernel before falling back to a userspace copy as a last resort.
344      *
345      * @param listener to be periodically notified as the copy progresses.
346      * @param signal to signal if the copy should be cancelled early.
347      * @return number of bytes copied.
348      */
copy(@onNull FileDescriptor in, @NonNull FileDescriptor out, @Nullable ProgressListener listener, @Nullable CancellationSignal signal)349     public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out,
350             @Nullable ProgressListener listener, @Nullable CancellationSignal signal)
351             throws IOException {
352         return copy(in, out, listener, signal, Long.MAX_VALUE);
353     }
354 
355     /**
356      * Copy the contents of one FD to another.
357      * <p>
358      * Attempts to use several optimization strategies to copy the data in the
359      * kernel before falling back to a userspace copy as a last resort.
360      *
361      * @param listener to be periodically notified as the copy progresses.
362      * @param signal to signal if the copy should be cancelled early.
363      * @param count the number of bytes to copy.
364      * @return number of bytes copied.
365      */
copy(@onNull FileDescriptor in, @NonNull FileDescriptor out, @Nullable ProgressListener listener, @Nullable CancellationSignal signal, long count)366     public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out,
367             @Nullable ProgressListener listener, @Nullable CancellationSignal signal, long count)
368             throws IOException {
369         if (ENABLE_COPY_OPTIMIZATIONS) {
370             try {
371                 final StructStat st_in = Os.fstat(in);
372                 final StructStat st_out = Os.fstat(out);
373                 if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) {
374                     return copyInternalSendfile(in, out, listener, signal, count);
375                 } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) {
376                     return copyInternalSplice(in, out, listener, signal, count);
377                 }
378             } catch (ErrnoException e) {
379                 throw e.rethrowAsIOException();
380             }
381         }
382 
383         // Worse case fallback to userspace
384         return copyInternalUserspace(in, out, listener, signal, count);
385     }
386 
387     /**
388      * Requires one of input or output to be a pipe.
389      */
390     @VisibleForTesting
copyInternalSplice(FileDescriptor in, FileDescriptor out, ProgressListener listener, CancellationSignal signal, long count)391     public static long copyInternalSplice(FileDescriptor in, FileDescriptor out,
392             ProgressListener listener, CancellationSignal signal, long count)
393             throws ErrnoException {
394         long progress = 0;
395         long checkpoint = 0;
396 
397         long t;
398         while ((t = Os.splice(in, null, out, null, Math.min(count, COPY_CHECKPOINT_BYTES),
399                 SPLICE_F_MOVE | SPLICE_F_MORE)) != 0) {
400             progress += t;
401             checkpoint += t;
402             count -= t;
403 
404             if (checkpoint >= COPY_CHECKPOINT_BYTES) {
405                 if (signal != null) {
406                     signal.throwIfCanceled();
407                 }
408                 if (listener != null) {
409                     listener.onProgress(progress);
410                 }
411                 checkpoint = 0;
412             }
413         }
414         if (listener != null) {
415             listener.onProgress(progress);
416         }
417         return progress;
418     }
419 
420     /**
421      * Requires both input and output to be a regular file.
422      */
423     @VisibleForTesting
copyInternalSendfile(FileDescriptor in, FileDescriptor out, ProgressListener listener, CancellationSignal signal, long count)424     public static long copyInternalSendfile(FileDescriptor in, FileDescriptor out,
425             ProgressListener listener, CancellationSignal signal, long count)
426             throws ErrnoException {
427         long progress = 0;
428         long checkpoint = 0;
429 
430         long t;
431         while ((t = Os.sendfile(out, in, null, Math.min(count, COPY_CHECKPOINT_BYTES))) != 0) {
432             progress += t;
433             checkpoint += t;
434             count -= t;
435 
436             if (checkpoint >= COPY_CHECKPOINT_BYTES) {
437                 if (signal != null) {
438                     signal.throwIfCanceled();
439                 }
440                 if (listener != null) {
441                     listener.onProgress(progress);
442                 }
443                 checkpoint = 0;
444             }
445         }
446         if (listener != null) {
447             listener.onProgress(progress);
448         }
449         return progress;
450     }
451 
452     @VisibleForTesting
copyInternalUserspace(FileDescriptor in, FileDescriptor out, ProgressListener listener, CancellationSignal signal, long count)453     public static long copyInternalUserspace(FileDescriptor in, FileDescriptor out,
454             ProgressListener listener, CancellationSignal signal, long count) throws IOException {
455         if (count != Long.MAX_VALUE) {
456             return copyInternalUserspace(new SizedInputStream(new FileInputStream(in), count),
457                     new FileOutputStream(out), listener, signal);
458         } else {
459             return copyInternalUserspace(new FileInputStream(in),
460                     new FileOutputStream(out), listener, signal);
461         }
462     }
463 
464     @VisibleForTesting
copyInternalUserspace(InputStream in, OutputStream out, ProgressListener listener, CancellationSignal signal)465     public static long copyInternalUserspace(InputStream in, OutputStream out,
466             ProgressListener listener, CancellationSignal signal) throws IOException {
467         long progress = 0;
468         long checkpoint = 0;
469         byte[] buffer = new byte[8192];
470 
471         int t;
472         while ((t = in.read(buffer)) != -1) {
473             out.write(buffer, 0, t);
474 
475             progress += t;
476             checkpoint += t;
477 
478             if (checkpoint >= COPY_CHECKPOINT_BYTES) {
479                 if (signal != null) {
480                     signal.throwIfCanceled();
481                 }
482                 if (listener != null) {
483                     listener.onProgress(progress);
484                 }
485                 checkpoint = 0;
486             }
487         }
488         if (listener != null) {
489             listener.onProgress(progress);
490         }
491         return progress;
492     }
493 
494     /**
495      * Check if a filename is "safe" (no metacharacters or spaces).
496      * @param file  The file to check
497      */
isFilenameSafe(File file)498     public static boolean isFilenameSafe(File file) {
499         // Note, we check whether it matches what's known to be safe,
500         // rather than what's known to be unsafe.  Non-ASCII, control
501         // characters, etc. are all unsafe by default.
502         return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches();
503     }
504 
505     /**
506      * Read a text file into a String, optionally limiting the length.
507      * @param file to read (will not seek, so things like /proc files are OK)
508      * @param max length (positive for head, negative of tail, 0 for no limit)
509      * @param ellipsis to add of the file was truncated (can be null)
510      * @return the contents of the file, possibly truncated
511      * @throws IOException if something goes wrong reading the file
512      */
readTextFile(File file, int max, String ellipsis)513     public static String readTextFile(File file, int max, String ellipsis) throws IOException {
514         InputStream input = new FileInputStream(file);
515         // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
516         // input stream, bytes read not equal to buffer size is not necessarily the correct
517         // indication for EOF; but it is true for BufferedInputStream due to its implementation.
518         BufferedInputStream bis = new BufferedInputStream(input);
519         try {
520             long size = file.length();
521             if (max > 0 || (size > 0 && max == 0)) {  // "head" mode: read the first N bytes
522                 if (size > 0 && (max == 0 || size < max)) max = (int) size;
523                 byte[] data = new byte[max + 1];
524                 int length = bis.read(data);
525                 if (length <= 0) return "";
526                 if (length <= max) return new String(data, 0, length);
527                 if (ellipsis == null) return new String(data, 0, max);
528                 return new String(data, 0, max) + ellipsis;
529             } else if (max < 0) {  // "tail" mode: keep the last N
530                 int len;
531                 boolean rolled = false;
532                 byte[] last = null;
533                 byte[] data = null;
534                 do {
535                     if (last != null) rolled = true;
536                     byte[] tmp = last; last = data; data = tmp;
537                     if (data == null) data = new byte[-max];
538                     len = bis.read(data);
539                 } while (len == data.length);
540 
541                 if (last == null && len <= 0) return "";
542                 if (last == null) return new String(data, 0, len);
543                 if (len > 0) {
544                     rolled = true;
545                     System.arraycopy(last, len, last, 0, last.length - len);
546                     System.arraycopy(data, 0, last, last.length - len, len);
547                 }
548                 if (ellipsis == null || !rolled) return new String(last);
549                 return ellipsis + new String(last);
550             } else {  // "cat" mode: size unknown, read it all in streaming fashion
551                 ByteArrayOutputStream contents = new ByteArrayOutputStream();
552                 int len;
553                 byte[] data = new byte[1024];
554                 do {
555                     len = bis.read(data);
556                     if (len > 0) contents.write(data, 0, len);
557                 } while (len == data.length);
558                 return contents.toString();
559             }
560         } finally {
561             bis.close();
562             input.close();
563         }
564     }
565 
stringToFile(File file, String string)566     public static void stringToFile(File file, String string) throws IOException {
567         stringToFile(file.getAbsolutePath(), string);
568     }
569 
570     /*
571      * Writes the bytes given in {@code content} to the file whose absolute path
572      * is {@code filename}.
573      */
bytesToFile(String filename, byte[] content)574     public static void bytesToFile(String filename, byte[] content) throws IOException {
575         if (filename.startsWith("/proc/")) {
576             final int oldMask = StrictMode.allowThreadDiskWritesMask();
577             try (FileOutputStream fos = new FileOutputStream(filename)) {
578                 fos.write(content);
579             } finally {
580                 StrictMode.setThreadPolicyMask(oldMask);
581             }
582         } else {
583             try (FileOutputStream fos = new FileOutputStream(filename)) {
584                 fos.write(content);
585             }
586         }
587     }
588 
589     /**
590      * Writes string to file. Basically same as "echo -n $string > $filename"
591      *
592      * @param filename
593      * @param string
594      * @throws IOException
595      */
stringToFile(String filename, String string)596     public static void stringToFile(String filename, String string) throws IOException {
597         bytesToFile(filename, string.getBytes(StandardCharsets.UTF_8));
598     }
599 
600     /**
601      * Computes the checksum of a file using the CRC32 checksum routine.
602      * The value of the checksum is returned.
603      *
604      * @param file  the file to checksum, must not be null
605      * @return the checksum value or an exception is thrown.
606      */
checksumCrc32(File file)607     public static long checksumCrc32(File file) throws FileNotFoundException, IOException {
608         CRC32 checkSummer = new CRC32();
609         CheckedInputStream cis = null;
610 
611         try {
612             cis = new CheckedInputStream( new FileInputStream(file), checkSummer);
613             byte[] buf = new byte[128];
614             while(cis.read(buf) >= 0) {
615                 // Just read for checksum to get calculated.
616             }
617             return checkSummer.getValue();
618         } finally {
619             if (cis != null) {
620                 try {
621                     cis.close();
622                 } catch (IOException e) {
623                 }
624             }
625         }
626     }
627 
628     /**
629      * Delete older files in a directory until only those matching the given
630      * constraints remain.
631      *
632      * @param minCount Always keep at least this many files.
633      * @param minAgeMs Always keep files younger than this age, in milliseconds.
634      * @return if any files were deleted.
635      */
deleteOlderFiles(File dir, int minCount, long minAgeMs)636     public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) {
637         if (minCount < 0 || minAgeMs < 0) {
638             throw new IllegalArgumentException("Constraints must be positive or 0");
639         }
640 
641         final File[] files = dir.listFiles();
642         if (files == null) return false;
643 
644         // Sort with newest files first
645         Arrays.sort(files, new Comparator<File>() {
646             @Override
647             public int compare(File lhs, File rhs) {
648                 return Long.compare(rhs.lastModified(), lhs.lastModified());
649             }
650         });
651 
652         // Keep at least minCount files
653         boolean deleted = false;
654         for (int i = minCount; i < files.length; i++) {
655             final File file = files[i];
656 
657             // Keep files newer than minAgeMs
658             final long age = System.currentTimeMillis() - file.lastModified();
659             if (age > minAgeMs) {
660                 if (file.delete()) {
661                     Log.d(TAG, "Deleted old file " + file);
662                     deleted = true;
663                 }
664             }
665         }
666         return deleted;
667     }
668 
669     /**
670      * Test if a file lives under the given directory, either as a direct child
671      * or a distant grandchild.
672      * <p>
673      * Both files <em>must</em> have been resolved using
674      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
675      * attacks.
676      */
contains(File[] dirs, File file)677     public static boolean contains(File[] dirs, File file) {
678         for (File dir : dirs) {
679             if (contains(dir, file)) {
680                 return true;
681             }
682         }
683         return false;
684     }
685 
686     /**
687      * Test if a file lives under the given directory, either as a direct child
688      * or a distant grandchild.
689      * <p>
690      * Both files <em>must</em> have been resolved using
691      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
692      * attacks.
693      */
contains(File dir, File file)694     public static boolean contains(File dir, File file) {
695         if (dir == null || file == null) return false;
696         return contains(dir.getAbsolutePath(), file.getAbsolutePath());
697     }
698 
contains(String dirPath, String filePath)699     public static boolean contains(String dirPath, String filePath) {
700         if (dirPath.equals(filePath)) {
701             return true;
702         }
703         if (!dirPath.endsWith("/")) {
704             dirPath += "/";
705         }
706         return filePath.startsWith(dirPath);
707     }
708 
deleteContentsAndDir(File dir)709     public static boolean deleteContentsAndDir(File dir) {
710         if (deleteContents(dir)) {
711             return dir.delete();
712         } else {
713             return false;
714         }
715     }
716 
deleteContents(File dir)717     public static boolean deleteContents(File dir) {
718         File[] files = dir.listFiles();
719         boolean success = true;
720         if (files != null) {
721             for (File file : files) {
722                 if (file.isDirectory()) {
723                     success &= deleteContents(file);
724                 }
725                 if (!file.delete()) {
726                     Log.w(TAG, "Failed to delete " + file);
727                     success = false;
728                 }
729             }
730         }
731         return success;
732     }
733 
isValidExtFilenameChar(char c)734     private static boolean isValidExtFilenameChar(char c) {
735         switch (c) {
736             case '\0':
737             case '/':
738                 return false;
739             default:
740                 return true;
741         }
742     }
743 
744     /**
745      * Check if given filename is valid for an ext4 filesystem.
746      */
isValidExtFilename(String name)747     public static boolean isValidExtFilename(String name) {
748         return (name != null) && name.equals(buildValidExtFilename(name));
749     }
750 
751     /**
752      * Mutate the given filename to make it valid for an ext4 filesystem,
753      * replacing any invalid characters with "_".
754      */
buildValidExtFilename(String name)755     public static String buildValidExtFilename(String name) {
756         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
757             return "(invalid)";
758         }
759         final StringBuilder res = new StringBuilder(name.length());
760         for (int i = 0; i < name.length(); i++) {
761             final char c = name.charAt(i);
762             if (isValidExtFilenameChar(c)) {
763                 res.append(c);
764             } else {
765                 res.append('_');
766             }
767         }
768         trimFilename(res, 255);
769         return res.toString();
770     }
771 
isValidFatFilenameChar(char c)772     private static boolean isValidFatFilenameChar(char c) {
773         if ((0x00 <= c && c <= 0x1f)) {
774             return false;
775         }
776         switch (c) {
777             case '"':
778             case '*':
779             case '/':
780             case ':':
781             case '<':
782             case '>':
783             case '?':
784             case '\\':
785             case '|':
786             case 0x7F:
787                 return false;
788             default:
789                 return true;
790         }
791     }
792 
793     /**
794      * Check if given filename is valid for a FAT filesystem.
795      */
isValidFatFilename(String name)796     public static boolean isValidFatFilename(String name) {
797         return (name != null) && name.equals(buildValidFatFilename(name));
798     }
799 
800     /**
801      * Mutate the given filename to make it valid for a FAT filesystem,
802      * replacing any invalid characters with "_".
803      */
buildValidFatFilename(String name)804     public static String buildValidFatFilename(String name) {
805         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
806             return "(invalid)";
807         }
808         final StringBuilder res = new StringBuilder(name.length());
809         for (int i = 0; i < name.length(); i++) {
810             final char c = name.charAt(i);
811             if (isValidFatFilenameChar(c)) {
812                 res.append(c);
813             } else {
814                 res.append('_');
815             }
816         }
817         // Even though vfat allows 255 UCS-2 chars, we might eventually write to
818         // ext4 through a FUSE layer, so use that limit.
819         trimFilename(res, 255);
820         return res.toString();
821     }
822 
823     @VisibleForTesting
trimFilename(String str, int maxBytes)824     public static String trimFilename(String str, int maxBytes) {
825         final StringBuilder res = new StringBuilder(str);
826         trimFilename(res, maxBytes);
827         return res.toString();
828     }
829 
trimFilename(StringBuilder res, int maxBytes)830     private static void trimFilename(StringBuilder res, int maxBytes) {
831         byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
832         if (raw.length > maxBytes) {
833             maxBytes -= 3;
834             while (raw.length > maxBytes) {
835                 res.deleteCharAt(res.length() / 2);
836                 raw = res.toString().getBytes(StandardCharsets.UTF_8);
837             }
838             res.insert(res.length() / 2, "...");
839         }
840     }
841 
rewriteAfterRename(File beforeDir, File afterDir, String path)842     public static String rewriteAfterRename(File beforeDir, File afterDir, String path) {
843         if (path == null) return null;
844         final File result = rewriteAfterRename(beforeDir, afterDir, new File(path));
845         return (result != null) ? result.getAbsolutePath() : null;
846     }
847 
rewriteAfterRename(File beforeDir, File afterDir, String[] paths)848     public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) {
849         if (paths == null) return null;
850         final String[] result = new String[paths.length];
851         for (int i = 0; i < paths.length; i++) {
852             result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]);
853         }
854         return result;
855     }
856 
857     /**
858      * Given a path under the "before" directory, rewrite it to live under the
859      * "after" directory. For example, {@code /before/foo/bar.txt} would become
860      * {@code /after/foo/bar.txt}.
861      */
rewriteAfterRename(File beforeDir, File afterDir, File file)862     public static File rewriteAfterRename(File beforeDir, File afterDir, File file) {
863         if (file == null || beforeDir == null || afterDir == null) return null;
864         if (contains(beforeDir, file)) {
865             final String splice = file.getAbsolutePath().substring(
866                     beforeDir.getAbsolutePath().length());
867             return new File(afterDir, splice);
868         }
869         return null;
870     }
871 
buildUniqueFileWithExtension(File parent, String name, String ext)872     private static File buildUniqueFileWithExtension(File parent, String name, String ext)
873             throws FileNotFoundException {
874         File file = buildFile(parent, name, ext);
875 
876         // If conflicting file, try adding counter suffix
877         int n = 0;
878         while (file.exists()) {
879             if (n++ >= 32) {
880                 throw new FileNotFoundException("Failed to create unique file");
881             }
882             file = buildFile(parent, name + " (" + n + ")", ext);
883         }
884 
885         return file;
886     }
887 
888     /**
889      * Generates a unique file name under the given parent directory. If the display name doesn't
890      * have an extension that matches the requested MIME type, the default extension for that MIME
891      * type is appended. If a file already exists, the name is appended with a numerical value to
892      * make it unique.
893      *
894      * For example, the display name 'example' with 'text/plain' MIME might produce
895      * 'example.txt' or 'example (1).txt', etc.
896      *
897      * @throws FileNotFoundException
898      */
buildUniqueFile(File parent, String mimeType, String displayName)899     public static File buildUniqueFile(File parent, String mimeType, String displayName)
900             throws FileNotFoundException {
901         final String[] parts = splitFileName(mimeType, displayName);
902         return buildUniqueFileWithExtension(parent, parts[0], parts[1]);
903     }
904 
905     /**
906      * Generates a unique file name under the given parent directory, keeping
907      * any extension intact.
908      */
buildUniqueFile(File parent, String displayName)909     public static File buildUniqueFile(File parent, String displayName)
910             throws FileNotFoundException {
911         final String name;
912         final String ext;
913 
914         // Extract requested extension from display name
915         final int lastDot = displayName.lastIndexOf('.');
916         if (lastDot >= 0) {
917             name = displayName.substring(0, lastDot);
918             ext = displayName.substring(lastDot + 1);
919         } else {
920             name = displayName;
921             ext = null;
922         }
923 
924         return buildUniqueFileWithExtension(parent, name, ext);
925     }
926 
927     /**
928      * Splits file name into base name and extension.
929      * If the display name doesn't have an extension that matches the requested MIME type, the
930      * extension is regarded as a part of filename and default extension for that MIME type is
931      * appended.
932      */
splitFileName(String mimeType, String displayName)933     public static String[] splitFileName(String mimeType, String displayName) {
934         String name;
935         String ext;
936 
937         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
938             name = displayName;
939             ext = null;
940         } else {
941             String mimeTypeFromExt;
942 
943             // Extract requested extension from display name
944             final int lastDot = displayName.lastIndexOf('.');
945             if (lastDot >= 0) {
946                 name = displayName.substring(0, lastDot);
947                 ext = displayName.substring(lastDot + 1);
948                 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
949                         ext.toLowerCase());
950             } else {
951                 name = displayName;
952                 ext = null;
953                 mimeTypeFromExt = null;
954             }
955 
956             if (mimeTypeFromExt == null) {
957                 mimeTypeFromExt = "application/octet-stream";
958             }
959 
960             final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(
961                     mimeType);
962             if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) {
963                 // Extension maps back to requested MIME type; allow it
964             } else {
965                 // No match; insist that create file matches requested MIME
966                 name = displayName;
967                 ext = extFromMimeType;
968             }
969         }
970 
971         if (ext == null) {
972             ext = "";
973         }
974 
975         return new String[] { name, ext };
976     }
977 
buildFile(File parent, String name, String ext)978     private static File buildFile(File parent, String name, String ext) {
979         if (TextUtils.isEmpty(ext)) {
980             return new File(parent, name);
981         } else {
982             return new File(parent, name + "." + ext);
983         }
984     }
985 
listOrEmpty(@ullable File dir)986     public static @NonNull String[] listOrEmpty(@Nullable File dir) {
987         if (dir == null) return EmptyArray.STRING;
988         final String[] res = dir.list();
989         if (res != null) {
990             return res;
991         } else {
992             return EmptyArray.STRING;
993         }
994     }
995 
listFilesOrEmpty(@ullable File dir)996     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
997         if (dir == null) return EMPTY;
998         final File[] res = dir.listFiles();
999         if (res != null) {
1000             return res;
1001         } else {
1002             return EMPTY;
1003         }
1004     }
1005 
listFilesOrEmpty(@ullable File dir, FilenameFilter filter)1006     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) {
1007         if (dir == null) return EMPTY;
1008         final File[] res = dir.listFiles(filter);
1009         if (res != null) {
1010             return res;
1011         } else {
1012             return EMPTY;
1013         }
1014     }
1015 
newFileOrNull(@ullable String path)1016     public static @Nullable File newFileOrNull(@Nullable String path) {
1017         return (path != null) ? new File(path) : null;
1018     }
1019 
1020     /**
1021      * Creates a directory with name {@code name} under an existing directory {@code baseDir}.
1022      * Returns a {@code File} object representing the directory on success, {@code null} on
1023      * failure.
1024      */
createDir(File baseDir, String name)1025     public static @Nullable File createDir(File baseDir, String name) {
1026         final File dir = new File(baseDir, name);
1027 
1028         if (dir.exists()) {
1029             return dir.isDirectory() ? dir : null;
1030         }
1031 
1032         return dir.mkdir() ? dir : null;
1033     }
1034 
1035     /**
1036      * Round the given size of a storage device to a nice round power-of-two
1037      * value, such as 256MB or 32GB. This avoids showing weird values like
1038      * "29.5GB" in UI.
1039      */
roundStorageSize(long size)1040     public static long roundStorageSize(long size) {
1041         long val = 1;
1042         long pow = 1;
1043         while ((val * pow) < size) {
1044             val <<= 1;
1045             if (val > 512) {
1046                 val = 1;
1047                 pow *= 1000;
1048             }
1049         }
1050         return val * pow;
1051     }
1052 
1053     @VisibleForTesting
1054     public static class MemoryPipe extends Thread implements AutoCloseable {
1055         private final FileDescriptor[] pipe;
1056         private final byte[] data;
1057         private final boolean sink;
1058 
MemoryPipe(byte[] data, boolean sink)1059         private MemoryPipe(byte[] data, boolean sink) throws IOException {
1060             try {
1061                 this.pipe = Os.pipe();
1062             } catch (ErrnoException e) {
1063                 throw e.rethrowAsIOException();
1064             }
1065             this.data = data;
1066             this.sink = sink;
1067         }
1068 
startInternal()1069         private MemoryPipe startInternal() {
1070             super.start();
1071             return this;
1072         }
1073 
createSource(byte[] data)1074         public static MemoryPipe createSource(byte[] data) throws IOException {
1075             return new MemoryPipe(data, false).startInternal();
1076         }
1077 
createSink(byte[] data)1078         public static MemoryPipe createSink(byte[] data) throws IOException {
1079             return new MemoryPipe(data, true).startInternal();
1080         }
1081 
getFD()1082         public FileDescriptor getFD() {
1083             return sink ? pipe[1] : pipe[0];
1084         }
1085 
getInternalFD()1086         public FileDescriptor getInternalFD() {
1087             return sink ? pipe[0] : pipe[1];
1088         }
1089 
1090         @Override
run()1091         public void run() {
1092             final FileDescriptor fd = getInternalFD();
1093             try {
1094                 int i = 0;
1095                 while (i < data.length) {
1096                     if (sink) {
1097                         i += Os.read(fd, data, i, data.length - i);
1098                     } else {
1099                         i += Os.write(fd, data, i, data.length - i);
1100                     }
1101                 }
1102             } catch (IOException | ErrnoException e) {
1103                 // Ignored
1104             } finally {
1105                 if (sink) {
1106                     SystemClock.sleep(TimeUnit.SECONDS.toMillis(1));
1107                 }
1108                 IoUtils.closeQuietly(fd);
1109             }
1110         }
1111 
1112         @Override
close()1113         public void close() throws Exception {
1114             IoUtils.closeQuietly(getFD());
1115         }
1116     }
1117 }
1118