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