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