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