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
17package android.os;
18
19import static android.system.OsConstants.SPLICE_F_MORE;
20import static android.system.OsConstants.SPLICE_F_MOVE;
21import static android.system.OsConstants.S_ISFIFO;
22import static android.system.OsConstants.S_ISREG;
23
24import android.annotation.NonNull;
25import android.annotation.Nullable;
26import android.provider.DocumentsContract.Document;
27import android.system.ErrnoException;
28import android.system.Os;
29import android.system.StructStat;
30import android.text.TextUtils;
31import android.util.Log;
32import android.util.Slog;
33import android.webkit.MimeTypeMap;
34
35import com.android.internal.annotations.VisibleForTesting;
36import com.android.internal.util.SizedInputStream;
37
38import libcore.io.IoUtils;
39import libcore.util.EmptyArray;
40
41import java.io.BufferedInputStream;
42import java.io.ByteArrayOutputStream;
43import java.io.File;
44import java.io.FileDescriptor;
45import java.io.FileInputStream;
46import java.io.FileNotFoundException;
47import java.io.FileOutputStream;
48import java.io.FilenameFilter;
49import java.io.IOException;
50import java.io.InputStream;
51import java.io.OutputStream;
52import java.nio.charset.StandardCharsets;
53import java.util.Arrays;
54import java.util.Comparator;
55import java.util.Objects;
56import java.util.concurrent.TimeUnit;
57import java.util.regex.Pattern;
58import java.util.zip.CRC32;
59import java.util.zip.CheckedInputStream;
60
61/**
62 * Tools for managing files.  Not for public consumption.
63 * @hide
64 */
65public 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 {
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     */
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     */
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     */
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
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     */
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     */
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
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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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
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
709    public static boolean deleteContentsAndDir(File dir) {
710        if (deleteContents(dir)) {
711            return dir.delete();
712        } else {
713            return false;
714        }
715    }
716
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
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     */
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     */
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
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     */
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     */
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
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
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
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
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     */
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
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     */
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     */
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     */
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
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
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
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
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
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     */
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     */
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
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
1069        private MemoryPipe startInternal() {
1070            super.start();
1071            return this;
1072        }
1073
1074        public static MemoryPipe createSource(byte[] data) throws IOException {
1075            return new MemoryPipe(data, false).startInternal();
1076        }
1077
1078        public static MemoryPipe createSink(byte[] data) throws IOException {
1079            return new MemoryPipe(data, true).startInternal();
1080        }
1081
1082        public FileDescriptor getFD() {
1083            return sink ? pipe[1] : pipe[0];
1084        }
1085
1086        public FileDescriptor getInternalFD() {
1087            return sink ? pipe[0] : pipe[1];
1088        }
1089
1090        @Override
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
1113        public void close() throws Exception {
1114            IoUtils.closeQuietly(getFD());
1115        }
1116    }
1117}
1118