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