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