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